@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.
@@ -0,0 +1,2483 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.FeedbackSDK = {}));
5
+ })(this, (function (exports) { 'use strict';
6
+
7
+ class SDKError extends Error {
8
+ constructor(message, cause) {
9
+ super(message);
10
+ this.name = 'SDKError';
11
+ this.cause = cause;
12
+
13
+ if (Error.captureStackTrace) {
14
+ Error.captureStackTrace(this, SDKError);
15
+ }
16
+ }
17
+ }
18
+
19
+ class APIError extends Error {
20
+ constructor(status, message, response) {
21
+ super(message);
22
+ this.name = 'APIError';
23
+ this.status = status;
24
+ this.response = response;
25
+
26
+ if (Error.captureStackTrace) {
27
+ Error.captureStackTrace(this, APIError);
28
+ }
29
+ }
30
+
31
+ isNetworkError() {
32
+ return this.status === 0;
33
+ }
34
+
35
+ isClientError() {
36
+ return this.status >= 400 && this.status < 500;
37
+ }
38
+
39
+ isServerError() {
40
+ return this.status >= 500 && this.status < 600;
41
+ }
42
+ }
43
+
44
+ class WidgetError extends Error {
45
+ constructor(message, widgetType, widgetId) {
46
+ super(message);
47
+ this.name = 'WidgetError';
48
+ this.widgetType = widgetType;
49
+ this.widgetId = widgetId;
50
+
51
+ if (Error.captureStackTrace) {
52
+ Error.captureStackTrace(this, WidgetError);
53
+ }
54
+ }
55
+ }
56
+
57
+ class ConfigError extends Error {
58
+ constructor(message, configKey) {
59
+ super(message);
60
+ this.name = 'ConfigError';
61
+ this.configKey = configKey;
62
+
63
+ if (Error.captureStackTrace) {
64
+ Error.captureStackTrace(this, ConfigError);
65
+ }
66
+ }
67
+ }
68
+
69
+ class ValidationError extends Error {
70
+ constructor(message, field, value) {
71
+ super(message);
72
+ this.name = 'ValidationError';
73
+ this.field = field;
74
+ this.value = value;
75
+
76
+ if (Error.captureStackTrace) {
77
+ Error.captureStackTrace(this, ValidationError);
78
+ }
79
+ }
80
+ }
81
+
82
+ class APIService {
83
+ constructor(config = {}) {
84
+ this.workspace = config.workspace;
85
+ this.sessionToken = null;
86
+ this.sessionExpiry = null;
87
+ this.userContext = config.userContext || null;
88
+
89
+ // Construct workspace-specific API URL
90
+ if (config.apiUrl) {
91
+ // If explicitly provided, use it
92
+ this.baseURL = config.apiUrl;
93
+ } else if (this.workspace) {
94
+ // Construct from workspace: workspace.api.staging.product7.io/api/v1
95
+ this.baseURL = `https://${this.workspace}.api.staging.product7.io/api/v1`;
96
+ } else {
97
+ // Fallback to default
98
+ this.baseURL = 'https://api.staging.product7.io/api/v1';
99
+ }
100
+
101
+ console.log('[APIService] Using API URL:', this.baseURL);
102
+
103
+ // Try to load existing session from localStorage
104
+ this._loadStoredSession();
105
+ }
106
+
107
+ async init(userContext = null) {
108
+ console.log('[APIService] Starting initialization...');
109
+
110
+ if (userContext) {
111
+ this.userContext = userContext;
112
+ }
113
+
114
+ // Check if we have a valid session token
115
+ if (this.isSessionValid()) {
116
+ console.log('[APIService] Found valid existing session');
117
+ return { sessionToken: this.sessionToken };
118
+ }
119
+
120
+ // Try to get user context from various sources if not provided
121
+ if (!this.userContext) {
122
+ console.log(
123
+ '[APIService] No user context provided, attempting auto-detection...'
124
+ );
125
+ this.userContext = this._getUserContextFromStorage();
126
+ }
127
+
128
+ if (!this.userContext || !this.workspace) {
129
+ const error = `Missing ${!this.workspace ? 'workspace' : 'user context'} for initialization`;
130
+ console.error('[APIService]', error);
131
+ throw new APIError(400, error);
132
+ }
133
+
134
+ console.log('[APIService] User context detected:', this.userContext);
135
+
136
+ const payload = {
137
+ workspace: this.workspace,
138
+ user: this.userContext,
139
+ };
140
+
141
+ console.log(
142
+ '[APIService] Making init request to:',
143
+ `${this.baseURL}/widget/init`
144
+ );
145
+ console.log('[APIService] Payload:', payload);
146
+
147
+ try {
148
+ const response = await this._makeRequest('/widget/init', {
149
+ method: 'POST',
150
+ body: JSON.stringify(payload),
151
+ headers: {
152
+ 'Content-Type': 'application/json',
153
+ },
154
+ });
155
+
156
+ console.log('[APIService] Init response:', response);
157
+
158
+ // Store session token and expiry
159
+ this.sessionToken = response.session_token;
160
+ this.sessionExpiry = new Date(Date.now() + response.expires_in * 1000);
161
+
162
+ // Store session in localStorage for persistence
163
+ this._storeSession();
164
+
165
+ return {
166
+ sessionToken: this.sessionToken,
167
+ config: response.config || {},
168
+ expiresIn: response.expires_in,
169
+ };
170
+ } catch (error) {
171
+ console.error('[APIService] Init failed:', error);
172
+ throw new APIError(
173
+ error.status || 500,
174
+ `Failed to initialize widget: ${error.message}`,
175
+ error.response
176
+ );
177
+ }
178
+ }
179
+
180
+ async submitFeedback(feedbackData) {
181
+ // Ensure we have a valid session
182
+ if (!this.isSessionValid()) {
183
+ await this.init();
184
+ }
185
+
186
+ if (!this.sessionToken) {
187
+ throw new APIError(401, 'No valid session token available');
188
+ }
189
+
190
+ const payload = {
191
+ board:
192
+ feedbackData.board_id || feedbackData.board || feedbackData.boardId,
193
+ title: feedbackData.title,
194
+ content: feedbackData.content,
195
+ attachments: feedbackData.attachments || [],
196
+ };
197
+
198
+ try {
199
+ const response = await this._makeRequest('/widget/feedback', {
200
+ method: 'POST',
201
+ body: JSON.stringify(payload),
202
+ headers: {
203
+ 'Content-Type': 'application/json',
204
+ Authorization: `Bearer ${this.sessionToken}`,
205
+ },
206
+ });
207
+
208
+ return response;
209
+ } catch (error) {
210
+ // If session expired, try to reinitialize once
211
+ if (error.status === 401) {
212
+ this.sessionToken = null;
213
+ this.sessionExpiry = null;
214
+ await this.init();
215
+
216
+ // Retry the request with new session
217
+ return this.submitFeedback(feedbackData);
218
+ }
219
+
220
+ throw new APIError(
221
+ error.status || 500,
222
+ `Failed to submit feedback: ${error.message}`,
223
+ error.response
224
+ );
225
+ }
226
+ }
227
+
228
+ isSessionValid() {
229
+ return (
230
+ this.sessionToken && this.sessionExpiry && new Date() < this.sessionExpiry
231
+ );
232
+ }
233
+
234
+ setUserContext(userContext) {
235
+ this.userContext = userContext;
236
+ // Store in localStorage for persistence
237
+ if (typeof localStorage !== 'undefined') {
238
+ localStorage.setItem(
239
+ 'feedbackSDK_userContext',
240
+ JSON.stringify(userContext)
241
+ );
242
+ }
243
+ }
244
+
245
+ getUserContext() {
246
+ return this.userContext;
247
+ }
248
+
249
+ clearSession() {
250
+ this.sessionToken = null;
251
+ this.sessionExpiry = null;
252
+ if (typeof localStorage !== 'undefined') {
253
+ localStorage.removeItem('feedbackSDK_session');
254
+ }
255
+ }
256
+
257
+ _getUserContextFromStorage() {
258
+ if (typeof localStorage === 'undefined') return null;
259
+
260
+ console.log('[APIService] Attempting to detect user from storage...');
261
+
262
+ try {
263
+ // Try to get from feedbackSDK specific storage first
264
+ const stored = localStorage.getItem('feedbackSDK_userContext');
265
+ if (stored) {
266
+ console.log('[APIService] Found user context in feedbackSDK storage');
267
+ return JSON.parse(stored);
268
+ }
269
+
270
+ // Try to get from window object (if set by parent application)
271
+ if (typeof window !== 'undefined' && window.FeedbackSDKUserContext) {
272
+ console.log(
273
+ '[APIService] Found user context in window.FeedbackSDKUserContext'
274
+ );
275
+ return window.FeedbackSDKUserContext;
276
+ }
277
+
278
+ // Check window.currentUser
279
+ if (typeof window !== 'undefined' && window.currentUser) {
280
+ console.log('[APIService] Found user context in window.currentUser');
281
+ return this._mapUserData(window.currentUser);
282
+ }
283
+
284
+ // Try to extract from existing session storage
285
+ const authSources = ['auth', 'currentUser', 'userSession', 'user'];
286
+
287
+ for (const key of authSources) {
288
+ const sessionData = localStorage.getItem(key);
289
+ if (sessionData) {
290
+ console.log(`[APIService] Found data in localStorage.${key}`);
291
+ const parsed = JSON.parse(sessionData);
292
+
293
+ // Handle nested user data (like {user: {...}, token: ...})
294
+ const userData = parsed.user || parsed;
295
+
296
+ if (userData && (userData.id || userData.user_id || userData.email)) {
297
+ console.log('[APIService] Successfully mapped user data');
298
+ return this._mapUserData(userData);
299
+ }
300
+ }
301
+ }
302
+
303
+ console.log('[APIService] No user context found in any storage location');
304
+ return null;
305
+ } catch (error) {
306
+ console.warn(
307
+ '[FeedbackSDK] Failed to load user context from storage:',
308
+ error
309
+ );
310
+ return null;
311
+ }
312
+ }
313
+
314
+ _mapUserData(userData) {
315
+ console.log('[APIService] Mapping user data:', userData);
316
+
317
+ const mapped = {
318
+ user_id: userData.id || userData.user_id || userData.userId,
319
+ email: userData.email,
320
+ name: userData.name || userData.displayName || userData.full_name,
321
+ custom_fields: {},
322
+ company: {},
323
+ };
324
+
325
+ // Map company data if available
326
+ if (userData.company || userData.organization) {
327
+ const company = userData.company || userData.organization;
328
+ mapped.company = {
329
+ id: company.id || company.company_id,
330
+ name: company.name || company.company_name,
331
+ monthly_spend: company.monthly_spend || company.spend,
332
+ };
333
+ }
334
+
335
+ // Map any additional custom fields
336
+ const customFieldKeys = ['plan', 'role', 'tier', 'subscription'];
337
+ customFieldKeys.forEach((key) => {
338
+ if (userData[key]) {
339
+ mapped.custom_fields[key] = userData[key];
340
+ }
341
+ });
342
+
343
+ console.log('[APIService] Mapped result:', mapped);
344
+ return mapped;
345
+ }
346
+
347
+ _storeSession() {
348
+ if (typeof localStorage === 'undefined') return;
349
+
350
+ try {
351
+ const sessionData = {
352
+ token: this.sessionToken,
353
+ expiry: this.sessionExpiry.toISOString(),
354
+ workspace: this.workspace,
355
+ };
356
+ localStorage.setItem('feedbackSDK_session', JSON.stringify(sessionData));
357
+ } catch (error) {
358
+ console.warn('[FeedbackSDK] Failed to store session:', error);
359
+ }
360
+ }
361
+
362
+ _loadStoredSession() {
363
+ if (typeof localStorage === 'undefined') return false;
364
+
365
+ try {
366
+ const stored = localStorage.getItem('feedbackSDK_session');
367
+ if (!stored) return false;
368
+
369
+ const sessionData = JSON.parse(stored);
370
+ this.sessionToken = sessionData.token;
371
+ this.sessionExpiry = new Date(sessionData.expiry);
372
+
373
+ return this.isSessionValid();
374
+ } catch (error) {
375
+ console.warn('[FeedbackSDK] Failed to load stored session:', error);
376
+ return false;
377
+ }
378
+ }
379
+
380
+ async _makeRequest(endpoint, options = {}) {
381
+ const url = `${this.baseURL}${endpoint}`;
382
+ console.log('[APIService] Making request to:', url);
383
+
384
+ try {
385
+ const response = await fetch(url, options);
386
+
387
+ if (!response.ok) {
388
+ let errorMessage = `HTTP ${response.status}`;
389
+ let responseData = null;
390
+
391
+ try {
392
+ responseData = await response.json();
393
+ errorMessage =
394
+ responseData.message || responseData.error || errorMessage;
395
+ } catch (e) {
396
+ errorMessage = (await response.text()) || errorMessage;
397
+ }
398
+
399
+ throw new APIError(response.status, errorMessage, responseData);
400
+ }
401
+
402
+ const contentType = response.headers.get('content-type');
403
+ if (contentType && contentType.includes('application/json')) {
404
+ return await response.json();
405
+ }
406
+
407
+ return await response.text();
408
+ } catch (error) {
409
+ if (error instanceof APIError) {
410
+ throw error;
411
+ }
412
+
413
+ // Network or other errors
414
+ throw new APIError(0, error.message, null);
415
+ }
416
+ }
417
+ }
418
+
419
+ class EventBus {
420
+ constructor() {
421
+ this.events = new Map();
422
+ }
423
+
424
+ on(event, callback) {
425
+ if (!this.events.has(event)) {
426
+ this.events.set(event, []);
427
+ }
428
+ this.events.get(event).push(callback);
429
+
430
+ return () => this.off(event, callback);
431
+ }
432
+
433
+ off(event, callback) {
434
+ const callbacks = this.events.get(event);
435
+ if (callbacks) {
436
+ const index = callbacks.indexOf(callback);
437
+ if (index > -1) {
438
+ callbacks.splice(index, 1);
439
+ }
440
+ }
441
+ }
442
+
443
+ emit(event, data) {
444
+ const callbacks = this.events.get(event);
445
+ if (callbacks) {
446
+ callbacks.forEach((callback) => {
447
+ try {
448
+ callback(data);
449
+ } catch (error) {
450
+ console.error('[FeedbackSDK] Event callback error:', error);
451
+ }
452
+ });
453
+ }
454
+ }
455
+
456
+ once(event, callback) {
457
+ const unsubscribe = this.on(event, (data) => {
458
+ callback(data);
459
+ unsubscribe();
460
+ });
461
+ return unsubscribe;
462
+ }
463
+
464
+ clear() {
465
+ this.events.clear();
466
+ }
467
+
468
+ getListenerCount(event) {
469
+ const callbacks = this.events.get(event);
470
+ return callbacks ? callbacks.length : 0;
471
+ }
472
+ }
473
+
474
+ function generateId(prefix = 'feedback') {
475
+ const timestamp = Date.now();
476
+ const random = Math.random().toString(36).substring(2, 9);
477
+ return `${prefix}_${timestamp}_${random}`;
478
+ }
479
+
480
+ function deepMerge(target, source) {
481
+ const result = { ...target };
482
+
483
+ for (const key in source) {
484
+ if (source.hasOwnProperty(key)) {
485
+ if (
486
+ source[key] &&
487
+ typeof source[key] === 'object' &&
488
+ !Array.isArray(source[key])
489
+ ) {
490
+ result[key] = deepMerge(target[key] || {}, source[key]);
491
+ } else {
492
+ result[key] = source[key];
493
+ }
494
+ }
495
+ }
496
+
497
+ return result;
498
+ }
499
+
500
+ function debounce(func, wait) {
501
+ let timeout;
502
+ return function executedFunction(...args) {
503
+ const later = () => {
504
+ clearTimeout(timeout);
505
+ func(...args);
506
+ };
507
+ clearTimeout(timeout);
508
+ timeout = setTimeout(later, wait);
509
+ };
510
+ }
511
+
512
+ function throttle(func, limit) {
513
+ let lastFunc;
514
+ let lastRan;
515
+ return function (...args) {
516
+ if (!lastRan) {
517
+ func(...args);
518
+ lastRan = Date.now();
519
+ } else {
520
+ clearTimeout(lastFunc);
521
+ lastFunc = setTimeout(
522
+ () => {
523
+ if (Date.now() - lastRan >= limit) {
524
+ func(...args);
525
+ lastRan = Date.now();
526
+ }
527
+ },
528
+ limit - (Date.now() - lastRan)
529
+ );
530
+ }
531
+ };
532
+ }
533
+
534
+ function isValidEmail(email) {
535
+ if (!email || typeof email !== 'string') return false;
536
+
537
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
538
+ return emailRegex.test(email.trim());
539
+ }
540
+
541
+ function sanitizeHTML(str) {
542
+ if (!str || typeof str !== 'string') return '';
543
+
544
+ const div = document.createElement('div');
545
+ div.textContent = str;
546
+ return div.innerHTML;
547
+ }
548
+
549
+ function getCSSProperty(element, property, fallback = '') {
550
+ if (!element || !property) return fallback;
551
+
552
+ try {
553
+ const style = window.getComputedStyle(element);
554
+ return style.getPropertyValue(property) || fallback;
555
+ } catch (error) {
556
+ return fallback;
557
+ }
558
+ }
559
+
560
+ function isInViewport(element) {
561
+ if (!element) return false;
562
+
563
+ const rect = element.getBoundingClientRect();
564
+ return (
565
+ rect.top >= 0 &&
566
+ rect.left >= 0 &&
567
+ rect.bottom <=
568
+ (window.innerHeight || document.documentElement.clientHeight) &&
569
+ rect.right <= (window.innerWidth || document.documentElement.clientWidth)
570
+ );
571
+ }
572
+
573
+ function scrollToElement(element, options = {}) {
574
+ if (!element) return;
575
+
576
+ const defaultOptions = {
577
+ behavior: 'smooth',
578
+ block: 'center',
579
+ inline: 'nearest',
580
+ };
581
+
582
+ element.scrollIntoView({ ...defaultOptions, ...options });
583
+ }
584
+
585
+ function getBrowserInfo() {
586
+ const userAgent = navigator.userAgent;
587
+ const platform = navigator.platform;
588
+
589
+ return {
590
+ userAgent,
591
+ platform,
592
+ language: navigator.language || navigator.userLanguage,
593
+ cookieEnabled: navigator.cookieEnabled,
594
+ screenResolution: `${screen.width}x${screen.height}`,
595
+ windowSize: `${window.innerWidth}x${window.innerHeight}`,
596
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
597
+ };
598
+ }
599
+
600
+ function formatFileSize(bytes) {
601
+ if (bytes === 0) return '0 Bytes';
602
+
603
+ const k = 1024;
604
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
605
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
606
+
607
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
608
+ }
609
+
610
+ function delay(ms) {
611
+ return new Promise((resolve) => setTimeout(resolve, ms));
612
+ }
613
+
614
+ function safeJsonParse(str, fallback = null) {
615
+ try {
616
+ return JSON.parse(str);
617
+ } catch (error) {
618
+ return fallback;
619
+ }
620
+ }
621
+
622
+ function escapeRegex(string) {
623
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
624
+ }
625
+
626
+ function getNestedProperty(obj, path, defaultValue = undefined) {
627
+ if (!obj || !path) return defaultValue;
628
+
629
+ const keys = path.split('.');
630
+ let current = obj;
631
+
632
+ for (const key of keys) {
633
+ if (current === null || current === undefined || !(key in current)) {
634
+ return defaultValue;
635
+ }
636
+ current = current[key];
637
+ }
638
+
639
+ return current;
640
+ }
641
+
642
+ function setNestedProperty(obj, path, value) {
643
+ if (!obj || !path) return obj;
644
+
645
+ const keys = path.split('.');
646
+ const lastKey = keys.pop();
647
+ let current = obj;
648
+
649
+ for (const key of keys) {
650
+ if (!(key in current) || typeof current[key] !== 'object') {
651
+ current[key] = {};
652
+ }
653
+ current = current[key];
654
+ }
655
+
656
+ current[lastKey] = value;
657
+ return obj;
658
+ }
659
+
660
+ function isBrowser() {
661
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
662
+ }
663
+
664
+ function isMobile() {
665
+ if (!isBrowser()) return false;
666
+
667
+ return (
668
+ /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
669
+ navigator.userAgent
670
+ ) || window.innerWidth <= 768
671
+ );
672
+ }
673
+
674
+ function getCurrentTimestamp() {
675
+ return new Date().toISOString();
676
+ }
677
+
678
+ function validateConfig(config, required = []) {
679
+ const missing = [];
680
+
681
+ for (const key of required) {
682
+ if (!config[key]) {
683
+ missing.push(key);
684
+ }
685
+ }
686
+
687
+ if (missing.length > 0) {
688
+ throw new Error(`Missing required configuration: ${missing.join(', ')}`);
689
+ }
690
+
691
+ return true;
692
+ }
693
+
694
+ var helpers = /*#__PURE__*/Object.freeze({
695
+ __proto__: null,
696
+ debounce: debounce,
697
+ deepMerge: deepMerge,
698
+ delay: delay,
699
+ escapeRegex: escapeRegex,
700
+ formatFileSize: formatFileSize,
701
+ generateId: generateId,
702
+ getBrowserInfo: getBrowserInfo,
703
+ getCSSProperty: getCSSProperty,
704
+ getCurrentTimestamp: getCurrentTimestamp,
705
+ getNestedProperty: getNestedProperty,
706
+ isBrowser: isBrowser,
707
+ isInViewport: isInViewport,
708
+ isMobile: isMobile,
709
+ isValidEmail: isValidEmail,
710
+ safeJsonParse: safeJsonParse,
711
+ sanitizeHTML: sanitizeHTML,
712
+ scrollToElement: scrollToElement,
713
+ setNestedProperty: setNestedProperty,
714
+ throttle: throttle,
715
+ validateConfig: validateConfig
716
+ });
717
+
718
+ class BaseWidget {
719
+ constructor(options = {}) {
720
+ this.id = options.id;
721
+ this.sdk = options.sdk;
722
+ this.apiService = options.apiService;
723
+ this.type = options.type || 'base';
724
+
725
+ this.options = {
726
+ container: null,
727
+ position: this.sdk.config.position,
728
+ theme: this.sdk.config.theme,
729
+ boardId: this.sdk.config.boardId,
730
+ autoShow: false,
731
+ customStyles: {},
732
+ ...options,
733
+ };
734
+
735
+ this.element = null;
736
+ this.modalElement = null;
737
+ this.mounted = false;
738
+ this.destroyed = false;
739
+
740
+ this.state = {
741
+ isOpen: false,
742
+ isSubmitting: false,
743
+ title: '',
744
+ content: '',
745
+ email: '',
746
+ attachments: [],
747
+ errors: {},
748
+ };
749
+
750
+ this._bindMethods();
751
+ }
752
+
753
+ mount(container) {
754
+ if (this.mounted || this.destroyed) return this;
755
+
756
+ if (typeof container === 'string') {
757
+ container = document.querySelector(container);
758
+ }
759
+
760
+ if (!container) {
761
+ container = document.body;
762
+ }
763
+
764
+ this.container = container;
765
+ this.element = this._render();
766
+ this.container.appendChild(this.element);
767
+
768
+ this.mounted = true;
769
+ this._attachEvents();
770
+ this.onMount();
771
+
772
+ if (this.options.autoShow) {
773
+ this.show();
774
+ }
775
+
776
+ this.sdk.eventBus.emit('widget:mounted', { widget: this });
777
+ return this;
778
+ }
779
+
780
+ show() {
781
+ if (this.element) {
782
+ this.element.style.display = 'block';
783
+ }
784
+ return this;
785
+ }
786
+
787
+ hide() {
788
+ if (this.element) {
789
+ this.element.style.display = 'none';
790
+ }
791
+ return this;
792
+ }
793
+
794
+ openModal() {
795
+ this.state.isOpen = true;
796
+ this._renderModal();
797
+ }
798
+
799
+ closeModal() {
800
+ this.state.isOpen = false;
801
+ if (this.modalElement) {
802
+ this.modalElement.remove();
803
+ this.modalElement = null;
804
+ }
805
+ this._resetForm();
806
+ }
807
+
808
+ async submitFeedback() {
809
+ if (this.state.isSubmitting) return;
810
+
811
+ try {
812
+ this.state.isSubmitting = true;
813
+ this._updateSubmitButton();
814
+
815
+ const payload = {
816
+ title: this.state.title || 'Feedback',
817
+ content: this.state.content,
818
+ email: this.state.email,
819
+ board_id: this.options.boardId,
820
+ attachments: this.state.attachments,
821
+ };
822
+
823
+ if (!this.state.content.trim()) {
824
+ this._showError('Please enter your feedback message.');
825
+ return;
826
+ }
827
+
828
+ const response = await this.apiService.submitFeedback(payload);
829
+
830
+ this._showSuccessMessage();
831
+ this.closeModal();
832
+
833
+ this.sdk.eventBus.emit('feedback:submitted', {
834
+ widget: this,
835
+ feedback: response,
836
+ });
837
+ } catch (error) {
838
+ this._showError('Failed to submit feedback. Please try again.');
839
+ this.sdk.eventBus.emit('feedback:error', { widget: this, error });
840
+ } finally {
841
+ this.state.isSubmitting = false;
842
+ this._updateSubmitButton();
843
+ }
844
+ }
845
+
846
+ handleConfigUpdate(newConfig) {
847
+ this.options.theme = newConfig.theme;
848
+ if (this.element) {
849
+ this._updateTheme();
850
+ }
851
+ }
852
+
853
+ destroy() {
854
+ if (this.destroyed) return;
855
+
856
+ this.onDestroy();
857
+ this.closeModal();
858
+
859
+ if (this.element && this.element.parentNode) {
860
+ this.element.parentNode.removeChild(this.element);
861
+ }
862
+
863
+ this.destroyed = true;
864
+ this.mounted = false;
865
+ this.sdk.eventBus.emit('widget:destroyed', { widget: this });
866
+ }
867
+
868
+ onMount() {}
869
+ onDestroy() {}
870
+
871
+ _render() {
872
+ throw new Error('_render() must be implemented by concrete widget');
873
+ }
874
+
875
+ _attachEvents() {
876
+ // Override in concrete widgets
877
+ }
878
+
879
+ _bindMethods() {
880
+ this.openModal = this.openModal.bind(this);
881
+ this.closeModal = this.closeModal.bind(this);
882
+ this.submitFeedback = this.submitFeedback.bind(this);
883
+ }
884
+
885
+ _renderModal() {
886
+ if (this.modalElement) return;
887
+
888
+ this.modalElement = document.createElement('div');
889
+ this.modalElement.className = `feedback-modal theme-${this.options.theme}`;
890
+ this.modalElement.innerHTML = this._getModalHTML();
891
+
892
+ document.body.appendChild(this.modalElement);
893
+ this._attachModalEvents();
894
+
895
+ const firstInput = this.modalElement.querySelector('input, textarea');
896
+ if (firstInput) {
897
+ setTimeout(() => firstInput.focus(), 100);
898
+ }
899
+ }
900
+
901
+ _getModalHTML() {
902
+ return `
903
+ <div class="feedback-modal-overlay">
904
+ <div class="feedback-modal-content">
905
+ <div class="feedback-modal-header">
906
+ <h3>Send Feedback</h3>
907
+ <button class="feedback-modal-close" type="button">&times;</button>
908
+ </div>
909
+ <form class="feedback-form">
910
+ <div class="feedback-form-group">
911
+ <label for="feedback-title-${this.id}">Title</label>
912
+ <input
913
+ type="text"
914
+ id="feedback-title-${this.id}"
915
+ name="title"
916
+ placeholder="Brief description of your feedback"
917
+ value="${this.state.title}"
918
+ />
919
+ </div>
920
+ <div class="feedback-form-group">
921
+ <label for="feedback-content-${this.id}">Message *</label>
922
+ <textarea
923
+ id="feedback-content-${this.id}"
924
+ name="content"
925
+ placeholder="Tell us more about your feedback..."
926
+ required
927
+ >${this.state.content}</textarea>
928
+ </div>
929
+ <div class="feedback-form-actions">
930
+ <button type="button" class="feedback-btn feedback-btn-cancel">Cancel</button>
931
+ <button type="submit" class="feedback-btn feedback-btn-submit">
932
+ ${this.state.isSubmitting ? 'Sending...' : 'Send Feedback'}
933
+ </button>
934
+ </div>
935
+ <div class="feedback-error" style="display: none;"></div>
936
+ </form>
937
+ </div>
938
+ </div>
939
+ `;
940
+ }
941
+
942
+ _attachModalEvents() {
943
+ const modal = this.modalElement;
944
+
945
+ modal
946
+ .querySelector('.feedback-modal-close')
947
+ .addEventListener('click', this.closeModal);
948
+ modal
949
+ .querySelector('.feedback-btn-cancel')
950
+ .addEventListener('click', this.closeModal);
951
+ modal
952
+ .querySelector('.feedback-modal-overlay')
953
+ .addEventListener('click', (e) => {
954
+ if (e.target === e.currentTarget) {
955
+ this.closeModal();
956
+ }
957
+ });
958
+
959
+ const form = modal.querySelector('.feedback-form');
960
+ form.addEventListener('submit', (e) => {
961
+ e.preventDefault();
962
+ this.submitFeedback();
963
+ });
964
+
965
+ modal
966
+ .querySelector('input[name="title"]')
967
+ .addEventListener('input', (e) => {
968
+ this.state.title = e.target.value;
969
+ });
970
+
971
+ modal
972
+ .querySelector('textarea[name="content"]')
973
+ .addEventListener('input', (e) => {
974
+ this.state.content = e.target.value;
975
+ });
976
+ }
977
+
978
+ _updateSubmitButton() {
979
+ if (this.modalElement) {
980
+ const submitBtn = this.modalElement.querySelector('.feedback-btn-submit');
981
+ if (submitBtn) {
982
+ submitBtn.textContent = this.state.isSubmitting
983
+ ? 'Sending...'
984
+ : 'Send Feedback';
985
+ submitBtn.disabled = this.state.isSubmitting;
986
+ }
987
+ }
988
+ }
989
+
990
+ _showError(message) {
991
+ if (this.modalElement) {
992
+ const errorElement = this.modalElement.querySelector('.feedback-error');
993
+ errorElement.textContent = message;
994
+ errorElement.style.display = 'block';
995
+ setTimeout(() => {
996
+ if (errorElement) {
997
+ errorElement.style.display = 'none';
998
+ }
999
+ }, 5000);
1000
+ }
1001
+ }
1002
+
1003
+ _showSuccessMessage() {
1004
+ const notification = document.createElement('div');
1005
+ notification.className = 'feedback-success-notification';
1006
+ notification.innerHTML = `
1007
+ <div class="feedback-success-content">
1008
+ <span>✓ Feedback submitted successfully!</span>
1009
+ <button class="feedback-success-close">&times;</button>
1010
+ </div>
1011
+ `;
1012
+
1013
+ document.body.appendChild(notification);
1014
+
1015
+ setTimeout(() => {
1016
+ if (notification.parentNode) {
1017
+ notification.parentNode.removeChild(notification);
1018
+ }
1019
+ }, 3000);
1020
+
1021
+ notification
1022
+ .querySelector('.feedback-success-close')
1023
+ .addEventListener('click', () => {
1024
+ if (notification.parentNode) {
1025
+ notification.parentNode.removeChild(notification);
1026
+ }
1027
+ });
1028
+ }
1029
+
1030
+ _resetForm() {
1031
+ this.state.title = '';
1032
+ this.state.content = '';
1033
+ this.state.email = '';
1034
+ this.state.errors = {};
1035
+ }
1036
+
1037
+ _updateTheme() {
1038
+ if (this.element) {
1039
+ this.element.className = this.element.className.replace(
1040
+ /theme-\w+/,
1041
+ `theme-${this.options.theme}`
1042
+ );
1043
+ }
1044
+ if (this.modalElement) {
1045
+ this.modalElement.className = this.modalElement.className.replace(
1046
+ /theme-\w+/,
1047
+ `theme-${this.options.theme}`
1048
+ );
1049
+ }
1050
+ }
1051
+ }
1052
+
1053
+ class ButtonWidget extends BaseWidget {
1054
+ constructor(options) {
1055
+ super({ ...options, type: 'button' });
1056
+ }
1057
+
1058
+ _render() {
1059
+ const button = document.createElement('div');
1060
+ button.className = `feedback-widget feedback-widget-button theme-${this.options.theme} position-${this.options.position}`;
1061
+ button.innerHTML = `
1062
+ <button class="feedback-trigger-btn" type="button">
1063
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
1064
+ <path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"/>
1065
+ <path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"/>
1066
+ </svg>
1067
+ Feedback
1068
+ </button>
1069
+ `;
1070
+
1071
+ if (this.options.customStyles) {
1072
+ Object.assign(button.style, this.options.customStyles);
1073
+ }
1074
+
1075
+ return button;
1076
+ }
1077
+
1078
+ _attachEvents() {
1079
+ const button = this.element.querySelector('.feedback-trigger-btn');
1080
+ button.addEventListener('click', this.openModal);
1081
+
1082
+ button.addEventListener('mouseenter', () => {
1083
+ if (!this.state.isSubmitting) {
1084
+ button.style.transform = 'translateY(-2px)';
1085
+ }
1086
+ });
1087
+
1088
+ button.addEventListener('mouseleave', () => {
1089
+ button.style.transform = 'translateY(0)';
1090
+ });
1091
+ }
1092
+
1093
+ updateText(text) {
1094
+ const button = this.element?.querySelector('.feedback-trigger-btn');
1095
+ if (button) {
1096
+ const textNode = button.childNodes[button.childNodes.length - 1];
1097
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
1098
+ textNode.textContent = text;
1099
+ }
1100
+ }
1101
+ }
1102
+
1103
+ updatePosition(position) {
1104
+ this.options.position = position;
1105
+ if (this.element) {
1106
+ this.element.className = this.element.className.replace(
1107
+ /position-\w+-\w+/,
1108
+ `position-${position}`
1109
+ );
1110
+ }
1111
+ }
1112
+ }
1113
+
1114
+ class InlineWidget extends BaseWidget {
1115
+ constructor(options) {
1116
+ super({ ...options, type: 'inline' });
1117
+ }
1118
+
1119
+ _render() {
1120
+ const widget = document.createElement('div');
1121
+ widget.className = `feedback-widget feedback-widget-inline theme-${this.options.theme}`;
1122
+ widget.innerHTML = `
1123
+ <div class="feedback-inline-content">
1124
+ <h3>Send us your feedback</h3>
1125
+ <form class="feedback-inline-form">
1126
+ <div class="feedback-form-group">
1127
+ <input
1128
+ type="text"
1129
+ name="title"
1130
+ placeholder="Title (optional)"
1131
+ value="${this.state.title}"
1132
+ />
1133
+ </div>
1134
+ <div class="feedback-form-group">
1135
+ <textarea
1136
+ name="content"
1137
+ placeholder="Your feedback..."
1138
+ required
1139
+ >${this.state.content}</textarea>
1140
+ </div>
1141
+ <div class="feedback-form-group">
1142
+ <input
1143
+ type="email"
1144
+ name="email"
1145
+ placeholder="Email (optional)"
1146
+ value="${this.state.email}"
1147
+ />
1148
+ </div>
1149
+ <button type="submit" class="feedback-btn feedback-btn-submit">
1150
+ Send Feedback
1151
+ </button>
1152
+ <div class="feedback-error" style="display: none;"></div>
1153
+ </form>
1154
+ </div>
1155
+ `;
1156
+
1157
+ if (this.options.customStyles) {
1158
+ Object.assign(widget.style, this.options.customStyles);
1159
+ }
1160
+
1161
+ return widget;
1162
+ }
1163
+
1164
+ _attachEvents() {
1165
+ const form = this.element.querySelector('.feedback-inline-form');
1166
+
1167
+ form.addEventListener('submit', (e) => {
1168
+ e.preventDefault();
1169
+ this.submitFeedback();
1170
+ });
1171
+
1172
+ form.querySelector('input[name="title"]').addEventListener('input', (e) => {
1173
+ this.state.title = e.target.value;
1174
+ });
1175
+
1176
+ form
1177
+ .querySelector('textarea[name="content"]')
1178
+ .addEventListener('input', (e) => {
1179
+ this.state.content = e.target.value;
1180
+ });
1181
+
1182
+ form.querySelector('input[name="email"]').addEventListener('input', (e) => {
1183
+ this.state.email = e.target.value;
1184
+ });
1185
+ }
1186
+
1187
+ openModal() {
1188
+ const textarea = this.element.querySelector('textarea[name="content"]');
1189
+ if (textarea) {
1190
+ textarea.focus();
1191
+ }
1192
+ }
1193
+
1194
+ closeModal() {
1195
+ // Inline widget doesn't use modal
1196
+ }
1197
+
1198
+ _showSuccessMessage() {
1199
+ const widget = this.element.querySelector('.feedback-inline-content');
1200
+ const originalContent = widget.innerHTML;
1201
+
1202
+ widget.innerHTML = `
1203
+ <div class="feedback-success">
1204
+ <div class="feedback-success-icon">✓</div>
1205
+ <h3>Thank you!</h3>
1206
+ <p>Your feedback has been submitted successfully.</p>
1207
+ <button class="feedback-btn feedback-btn-reset">Send Another</button>
1208
+ </div>
1209
+ `;
1210
+
1211
+ const resetBtn = widget.querySelector('.feedback-btn-reset');
1212
+ resetBtn.addEventListener('click', () => {
1213
+ widget.innerHTML = originalContent;
1214
+ this._attachEvents();
1215
+ this._resetForm();
1216
+ });
1217
+ }
1218
+
1219
+ _showError(message) {
1220
+ const errorElement = this.element.querySelector('.feedback-error');
1221
+ if (errorElement) {
1222
+ errorElement.textContent = message;
1223
+ errorElement.style.display = 'block';
1224
+
1225
+ setTimeout(() => {
1226
+ if (errorElement) {
1227
+ errorElement.style.display = 'none';
1228
+ }
1229
+ }, 5000);
1230
+ }
1231
+ }
1232
+
1233
+ _updateSubmitButton() {
1234
+ const submitBtn = this.element.querySelector('.feedback-btn-submit');
1235
+ if (submitBtn) {
1236
+ submitBtn.textContent = this.state.isSubmitting
1237
+ ? 'Sending...'
1238
+ : 'Send Feedback';
1239
+ submitBtn.disabled = this.state.isSubmitting;
1240
+ }
1241
+ }
1242
+
1243
+ updateTitle(title) {
1244
+ const titleElement = this.element?.querySelector('h3');
1245
+ if (titleElement) {
1246
+ titleElement.textContent = title;
1247
+ }
1248
+ }
1249
+
1250
+ setPlaceholder(field, placeholder) {
1251
+ const input = this.element?.querySelector(`[name="${field}"]`);
1252
+ if (input) {
1253
+ input.placeholder = placeholder;
1254
+ }
1255
+ }
1256
+ }
1257
+
1258
+ class TabWidget extends BaseWidget {
1259
+ constructor(options) {
1260
+ super({ ...options, type: 'tab' });
1261
+ }
1262
+
1263
+ _render() {
1264
+ const tab = document.createElement('div');
1265
+ tab.className = `feedback-widget feedback-widget-tab theme-${this.options.theme} position-${this.options.position}`;
1266
+ tab.innerHTML = `
1267
+ <div class="feedback-tab-trigger">
1268
+ <span class="feedback-tab-text">Feedback</span>
1269
+ </div>
1270
+ `;
1271
+
1272
+ if (this.options.customStyles) {
1273
+ Object.assign(tab.style, this.options.customStyles);
1274
+ }
1275
+
1276
+ return tab;
1277
+ }
1278
+
1279
+ _attachEvents() {
1280
+ const tab = this.element.querySelector('.feedback-tab-trigger');
1281
+ tab.addEventListener('click', this.openModal);
1282
+
1283
+ tab.addEventListener('mouseenter', () => {
1284
+ if (!this.state.isSubmitting) {
1285
+ tab.style.transform = this._getHoverTransform();
1286
+ }
1287
+ });
1288
+
1289
+ tab.addEventListener('mouseleave', () => {
1290
+ tab.style.transform = 'none';
1291
+ });
1292
+ }
1293
+
1294
+ _getHoverTransform() {
1295
+ const position = this.options.position;
1296
+ if (position.includes('right')) {
1297
+ return 'translateX(-5px)';
1298
+ } else if (position.includes('left')) {
1299
+ return 'translateX(5px)';
1300
+ }
1301
+ return 'none';
1302
+ }
1303
+
1304
+ updateText(text) {
1305
+ const textElement = this.element?.querySelector('.feedback-tab-text');
1306
+ if (textElement) {
1307
+ textElement.textContent = text;
1308
+ }
1309
+ }
1310
+
1311
+ updatePosition(position) {
1312
+ this.options.position = position;
1313
+ if (this.element) {
1314
+ this.element.className = this.element.className.replace(
1315
+ /position-\w+-\w+/,
1316
+ `position-${position}`
1317
+ );
1318
+ }
1319
+ }
1320
+ }
1321
+
1322
+ class WidgetFactory {
1323
+ static widgets = new Map([
1324
+ ['button', ButtonWidget],
1325
+ ['tab', TabWidget],
1326
+ ['inline', InlineWidget],
1327
+ ]);
1328
+
1329
+ static register(type, WidgetClass) {
1330
+ if (typeof type !== 'string' || !type.trim()) {
1331
+ throw new SDKError('Widget type must be a non-empty string');
1332
+ }
1333
+
1334
+ if (typeof WidgetClass !== 'function') {
1335
+ throw new SDKError('Widget class must be a constructor function');
1336
+ }
1337
+
1338
+ this.widgets.set(type, WidgetClass);
1339
+ }
1340
+
1341
+ static create(type, options = {}) {
1342
+ const WidgetClass = this.widgets.get(type);
1343
+
1344
+ if (!WidgetClass) {
1345
+ const availableTypes = Array.from(this.widgets.keys()).join(', ');
1346
+ throw new SDKError(
1347
+ `Unknown widget type: ${type}. Available types: ${availableTypes}`
1348
+ );
1349
+ }
1350
+
1351
+ try {
1352
+ return new WidgetClass(options);
1353
+ } catch (error) {
1354
+ throw new SDKError(
1355
+ `Failed to create widget of type '${type}': ${error.message}`,
1356
+ error
1357
+ );
1358
+ }
1359
+ }
1360
+
1361
+ static getAvailableTypes() {
1362
+ return Array.from(this.widgets.keys());
1363
+ }
1364
+
1365
+ static isTypeRegistered(type) {
1366
+ return this.widgets.has(type);
1367
+ }
1368
+
1369
+ static unregister(type) {
1370
+ return this.widgets.delete(type);
1371
+ }
1372
+
1373
+ static clear() {
1374
+ this.widgets.clear();
1375
+ }
1376
+
1377
+ static getWidgetClass(type) {
1378
+ return this.widgets.get(type);
1379
+ }
1380
+ }
1381
+
1382
+ class FeedbackSDK {
1383
+ constructor(config = {}) {
1384
+ this.config = this._validateAndMergeConfig(config);
1385
+ this.initialized = false;
1386
+ this.widgets = new Map();
1387
+ this.eventBus = new EventBus();
1388
+
1389
+ // Initialize API service
1390
+ this.apiService = new APIService({
1391
+ apiUrl: this.config.apiUrl,
1392
+ workspace: this.config.workspace,
1393
+ userContext: this.config.userContext,
1394
+ });
1395
+
1396
+ this._bindMethods();
1397
+ }
1398
+
1399
+ async init() {
1400
+ if (this.initialized) {
1401
+ return { alreadyInitialized: true };
1402
+ }
1403
+
1404
+ try {
1405
+ // Initialize the API service (this will handle the /widget/init call)
1406
+ const initData = await this.apiService.init(this.config.userContext);
1407
+
1408
+ // Merge any server-provided config with local config
1409
+ if (initData.config) {
1410
+ this.config = deepMerge(this.config, initData.config);
1411
+ }
1412
+
1413
+ this.initialized = true;
1414
+ this.eventBus.emit('sdk:initialized', {
1415
+ config: this.config,
1416
+ sessionToken: initData.sessionToken,
1417
+ });
1418
+
1419
+ return {
1420
+ initialized: true,
1421
+ config: initData.config || {},
1422
+ sessionToken: initData.sessionToken,
1423
+ expiresIn: initData.expiresIn,
1424
+ };
1425
+ } catch (error) {
1426
+ this.eventBus.emit('sdk:error', { error });
1427
+ throw new SDKError(`Failed to initialize SDK: ${error.message}`, error);
1428
+ }
1429
+ }
1430
+
1431
+ createWidget(type = 'button', options = {}) {
1432
+ if (!this.initialized) {
1433
+ throw new SDKError(
1434
+ 'SDK must be initialized before creating widgets. Call init() first.'
1435
+ );
1436
+ }
1437
+
1438
+ const widgetId = generateId('widget');
1439
+ const widgetOptions = {
1440
+ id: widgetId,
1441
+ sdk: this,
1442
+ apiService: this.apiService,
1443
+ ...this.config,
1444
+ ...options,
1445
+ };
1446
+
1447
+ try {
1448
+ const widget = WidgetFactory.create(type, widgetOptions);
1449
+ this.widgets.set(widgetId, widget);
1450
+
1451
+ this.eventBus.emit('widget:created', { widget, type });
1452
+ return widget;
1453
+ } catch (error) {
1454
+ throw new SDKError(`Failed to create widget: ${error.message}`, error);
1455
+ }
1456
+ }
1457
+
1458
+ getWidget(id) {
1459
+ return this.widgets.get(id);
1460
+ }
1461
+
1462
+ getAllWidgets() {
1463
+ return Array.from(this.widgets.values());
1464
+ }
1465
+
1466
+ destroyWidget(id) {
1467
+ const widget = this.widgets.get(id);
1468
+ if (widget) {
1469
+ widget.destroy();
1470
+ this.widgets.delete(id);
1471
+ this.eventBus.emit('widget:removed', { widgetId: id });
1472
+ return true;
1473
+ }
1474
+ return false;
1475
+ }
1476
+
1477
+ destroyAllWidgets() {
1478
+ for (const widget of this.widgets.values()) {
1479
+ widget.destroy();
1480
+ }
1481
+ this.widgets.clear();
1482
+ this.eventBus.emit('widgets:cleared');
1483
+ }
1484
+
1485
+ updateConfig(newConfig) {
1486
+ const oldConfig = { ...this.config };
1487
+ this.config = this._validateAndMergeConfig(newConfig, this.config);
1488
+
1489
+ // Update all existing widgets with new config
1490
+ for (const widget of this.widgets.values()) {
1491
+ widget.handleConfigUpdate(this.config);
1492
+ }
1493
+
1494
+ this.eventBus.emit('config:updated', {
1495
+ oldConfig,
1496
+ newConfig: this.config,
1497
+ });
1498
+ }
1499
+
1500
+ setUserContext(userContext) {
1501
+ this.config.userContext = userContext;
1502
+ if (this.apiService) {
1503
+ this.apiService.setUserContext(userContext);
1504
+ }
1505
+ this.eventBus.emit('user:updated', { userContext });
1506
+ }
1507
+
1508
+ getUserContext() {
1509
+ return (
1510
+ this.config.userContext ||
1511
+ (this.apiService ? this.apiService.getUserContext() : null)
1512
+ );
1513
+ }
1514
+
1515
+ async reinitialize(newUserContext = null) {
1516
+ // Clear current session
1517
+ this.apiService.clearSession();
1518
+ this.initialized = false;
1519
+
1520
+ // Update user context if provided
1521
+ if (newUserContext) {
1522
+ this.setUserContext(newUserContext);
1523
+ }
1524
+
1525
+ // Reinitialize
1526
+ return this.init();
1527
+ }
1528
+
1529
+ on(event, callback) {
1530
+ this.eventBus.on(event, callback);
1531
+ return this;
1532
+ }
1533
+
1534
+ off(event, callback) {
1535
+ this.eventBus.off(event, callback);
1536
+ return this;
1537
+ }
1538
+
1539
+ once(event, callback) {
1540
+ this.eventBus.once(event, callback);
1541
+ return this;
1542
+ }
1543
+
1544
+ emit(event, data) {
1545
+ this.eventBus.emit(event, data);
1546
+ return this;
1547
+ }
1548
+
1549
+ destroy() {
1550
+ this.destroyAllWidgets();
1551
+ this.eventBus.removeAllListeners();
1552
+ this.apiService.clearSession();
1553
+ this.initialized = false;
1554
+ this.eventBus.emit('sdk:destroyed');
1555
+ }
1556
+
1557
+ _validateAndMergeConfig(newConfig, existingConfig = {}) {
1558
+ const defaultConfig = {
1559
+ apiUrl: null,
1560
+ workspace: null,
1561
+ userContext: null,
1562
+ position: 'bottom-right',
1563
+ theme: 'light',
1564
+ boardId: 'general',
1565
+ autoShow: true,
1566
+ debug: false,
1567
+ };
1568
+
1569
+ const mergedConfig = deepMerge(
1570
+ deepMerge(defaultConfig, existingConfig),
1571
+ newConfig
1572
+ );
1573
+
1574
+ // Validate required config
1575
+ const requiredFields = ['workspace'];
1576
+ const missingFields = requiredFields.filter(
1577
+ (field) => !mergedConfig[field]
1578
+ );
1579
+
1580
+ if (missingFields.length > 0) {
1581
+ throw new ConfigError(
1582
+ `Missing required configuration: ${missingFields.join(', ')}`
1583
+ );
1584
+ }
1585
+
1586
+ // Validate userContext structure if provided
1587
+ if (mergedConfig.userContext) {
1588
+ this._validateUserContext(mergedConfig.userContext);
1589
+ }
1590
+
1591
+ return mergedConfig;
1592
+ }
1593
+
1594
+ _validateUserContext(userContext) {
1595
+ if (!userContext.user_id && !userContext.email) {
1596
+ throw new ConfigError(
1597
+ 'User context must include at least user_id or email'
1598
+ );
1599
+ }
1600
+
1601
+ // Validate structure matches expected API format
1602
+ const validStructure = {
1603
+ user_id: 'string',
1604
+ email: 'string',
1605
+ name: 'string',
1606
+ custom_fields: 'object',
1607
+ company: 'object',
1608
+ };
1609
+
1610
+ for (const [key, expectedType] of Object.entries(validStructure)) {
1611
+ if (userContext[key] && typeof userContext[key] !== expectedType) {
1612
+ throw new ConfigError(
1613
+ `User context field '${key}' must be of type '${expectedType}'`
1614
+ );
1615
+ }
1616
+ }
1617
+ }
1618
+
1619
+ _bindMethods() {
1620
+ this.createWidget = this.createWidget.bind(this);
1621
+ this.destroyWidget = this.destroyWidget.bind(this);
1622
+ this.updateConfig = this.updateConfig.bind(this);
1623
+ }
1624
+
1625
+ // Static helper methods
1626
+ static create(config) {
1627
+ return new FeedbackSDK(config);
1628
+ }
1629
+
1630
+ static async createAndInit(config) {
1631
+ const sdk = new FeedbackSDK(config);
1632
+ await sdk.init();
1633
+ return sdk;
1634
+ }
1635
+
1636
+ // Utility methods for external integrations
1637
+ static extractUserContextFromAuth(authData) {
1638
+ // Helper method to extract user context from common auth structures
1639
+ if (!authData) return null;
1640
+
1641
+ return {
1642
+ user_id: authData.sub || authData.id || authData.user_id,
1643
+ email: authData.email,
1644
+ name: authData.name || authData.display_name || authData.full_name,
1645
+ custom_fields: {
1646
+ role: authData.role,
1647
+ plan: authData.plan || authData.subscription?.plan,
1648
+ ...(authData.custom_fields || {}),
1649
+ },
1650
+ company:
1651
+ authData.company || authData.organization
1652
+ ? {
1653
+ id: authData.company?.id || authData.organization?.id,
1654
+ name: authData.company?.name || authData.organization?.name,
1655
+ monthly_spend: authData.company?.monthly_spend,
1656
+ }
1657
+ : undefined,
1658
+ };
1659
+ }
1660
+ }
1661
+
1662
+ const CSS_STYLES = `
1663
+ .feedback-widget {
1664
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', Oxygen, Ubuntu, Cantarell, sans-serif;
1665
+ font-size: 14px;
1666
+ line-height: 1.4;
1667
+ z-index: 999999;
1668
+ box-sizing: border-box;
1669
+ }
1670
+
1671
+ .feedback-widget *,
1672
+ .feedback-widget *::before,
1673
+ .feedback-widget *::after {
1674
+ box-sizing: border-box;
1675
+ }
1676
+
1677
+ .feedback-widget-button {
1678
+ position: fixed;
1679
+ z-index: 999999;
1680
+ }
1681
+
1682
+ .feedback-widget-button.position-bottom-right {
1683
+ bottom: 20px;
1684
+ right: 20px;
1685
+ }
1686
+
1687
+ .feedback-widget-button.position-bottom-left {
1688
+ bottom: 20px;
1689
+ left: 20px;
1690
+ }
1691
+
1692
+ .feedback-widget-button.position-top-right {
1693
+ top: 20px;
1694
+ right: 20px;
1695
+ }
1696
+
1697
+ .feedback-widget-button.position-top-left {
1698
+ top: 20px;
1699
+ left: 20px;
1700
+ }
1701
+
1702
+ .feedback-trigger-btn {
1703
+ position: relative;
1704
+ display: flex;
1705
+ align-items: center;
1706
+ justify-content: center;
1707
+ gap: 12px;
1708
+ height: 44px;
1709
+ overflow: hidden;
1710
+ border-radius: 0.5rem;
1711
+ border: none;
1712
+ padding: 10px 16px;
1713
+ font-size: 14px;
1714
+ font-weight: 500;
1715
+ font-family: inherit;
1716
+ cursor: pointer;
1717
+ transition: all 0.3s duration;
1718
+ color: white;
1719
+ background: #155EEF;
1720
+ box-shadow: 0 1px 2px 0 rgba(16, 24, 40, 0.05);
1721
+ }
1722
+
1723
+ .feedback-trigger-btn:hover:not(:disabled) {
1724
+ background: #004EEB;
1725
+ box-shadow: 0 1px 2px 0 rgba(16, 24, 40, 0.1);
1726
+ }
1727
+
1728
+ .feedback-trigger-btn:disabled {
1729
+ opacity: 0.7;
1730
+ cursor: not-allowed;
1731
+ }
1732
+
1733
+ .feedback-trigger-btn:focus-visible {
1734
+ outline: 2px solid #155EEF;
1735
+ outline-offset: 2px;
1736
+ }
1737
+
1738
+ .feedback-modal {
1739
+ position: fixed;
1740
+ top: 0;
1741
+ left: 0;
1742
+ right: 0;
1743
+ bottom: 0;
1744
+ z-index: 1000000;
1745
+ display: flex;
1746
+ align-items: center;
1747
+ justify-content: center;
1748
+ font-family: inherit;
1749
+ }
1750
+
1751
+ .feedback-modal-overlay {
1752
+ position: absolute;
1753
+ top: 0;
1754
+ left: 0;
1755
+ right: 0;
1756
+ bottom: 0;
1757
+ background: rgba(0, 0, 0, 0.5);
1758
+ display: flex;
1759
+ align-items: center;
1760
+ justify-content: center;
1761
+ }
1762
+
1763
+ .feedback-modal-content {
1764
+ background: white;
1765
+ border-radius: 8px;
1766
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
1767
+ min-width: 460px;
1768
+ max-width: 500px;
1769
+ width: 100%;
1770
+ padding: 16px;
1771
+ max-height: 85vh;
1772
+ overflow-y: hidden;
1773
+ position: relative;
1774
+ }
1775
+
1776
+ .feedback-modal.theme-dark .feedback-modal-content {
1777
+ background: #1F2937;
1778
+ color: white;
1779
+ }
1780
+
1781
+ .feedback-modal-header {
1782
+ display: flex;
1783
+ align-items: center;
1784
+ justify-content: space-between;
1785
+ padding: 16px;
1786
+ border-bottom: 1px solid #D1D5DB;
1787
+ flex-shrink: 0;
1788
+ }
1789
+
1790
+ .feedback-modal.theme-dark .feedback-modal-header {
1791
+ border-bottom-color: #374151;
1792
+ }
1793
+
1794
+ .feedback-modal-header h3 {
1795
+ margin: 0;
1796
+ font-size: 16px;
1797
+ font-weight: 600;
1798
+ }
1799
+
1800
+ .feedback-modal-close {
1801
+ background: none;
1802
+ border: none;
1803
+ font-size: 24px;
1804
+ cursor: pointer;
1805
+ color: #6B7280;
1806
+ padding: 0;
1807
+ width: 24px;
1808
+ height: 24px;
1809
+ display: flex;
1810
+ align-items: center;
1811
+ justify-content: center;
1812
+ transition: all 0.3s ease;
1813
+ }
1814
+
1815
+ .feedback-modal-close:hover {
1816
+ color: #374151;
1817
+ }
1818
+
1819
+ .feedback-modal-close:focus-visible {
1820
+ outline: 2px solid #155EEF;
1821
+ outline-offset: 2px;
1822
+ }
1823
+
1824
+ .feedback-modal.theme-dark .feedback-modal-close {
1825
+ color: #9CA3AF;
1826
+ }
1827
+
1828
+ .feedback-modal.theme-dark .feedback-modal-close:hover {
1829
+ color: #D1D5DB;
1830
+ }
1831
+
1832
+ .feedback-form {
1833
+ padding: 16px;
1834
+ }
1835
+
1836
+ .feedback-form-group {
1837
+ display: flex;
1838
+ flex-direction: column;
1839
+ gap: 4px;
1840
+ margin-bottom: 12px;
1841
+ }
1842
+
1843
+ .feedback-form-group:last-child {
1844
+ margin-bottom: 0;
1845
+ }
1846
+
1847
+ .feedback-form-group label {
1848
+ font-size: 14px;
1849
+ font-weight: 500;
1850
+ line-height: 1.25;
1851
+ color: #374151;
1852
+ }
1853
+
1854
+ .feedback-modal.theme-dark .feedback-form-group label {
1855
+ color: #D1D5DB;
1856
+ }
1857
+
1858
+ .feedback-form-group input {
1859
+ height: 40px;
1860
+ width: 100%;
1861
+ border-radius: 6px;
1862
+ border: 1px solid #D1D5DB;
1863
+ padding: 2px 12px;
1864
+ font-size: 14px;
1865
+ font-weight: 400;
1866
+ line-height: 1.25;
1867
+ color: #1F2937;
1868
+ font-family: inherit;
1869
+ outline: none;
1870
+ transition: all 0.2s ease;
1871
+ }
1872
+
1873
+ .feedback-form-group input::placeholder {
1874
+ font-size: 14px;
1875
+ color: #6B7280;
1876
+ }
1877
+
1878
+ .feedback-form-group input:focus {
1879
+ border-color: #84ADFF;
1880
+ box-shadow: 0 0 0 1px rgba(16, 24, 40, 0.05), 0 0 0 3px rgba(41, 112, 255, 0.2);
1881
+ }
1882
+
1883
+ .feedback-form-group input:focus-visible {
1884
+ outline: none;
1885
+ }
1886
+
1887
+ .feedback-form-group textarea {
1888
+ min-height: 100px;
1889
+ width: 100%;
1890
+ resize: both;
1891
+ border-radius: 6px;
1892
+ border: 1px solid #D1D5DB;
1893
+ padding: 2px 12px;
1894
+ font-size: 14px;
1895
+ font-weight: 400;
1896
+ line-height: 1.25;
1897
+ color: #1F2937;
1898
+ font-family: inherit;
1899
+ outline: none;
1900
+ transition: all 0.2s ease;
1901
+ }
1902
+
1903
+ .feedback-form-group textarea::placeholder {
1904
+ font-size: 14px;
1905
+ color: #6B7280;
1906
+ }
1907
+
1908
+ .feedback-form-group textarea:focus {
1909
+ border-color: #84ADFF;
1910
+ box-shadow: 0 0 0 1px rgba(16, 24, 40, 0.05), 0 0 0 3px rgba(41, 112, 255, 0.2);
1911
+ }
1912
+
1913
+ .feedback-form-group textarea:focus-visible {
1914
+ outline: none;
1915
+ }
1916
+
1917
+ .feedback-modal.theme-dark .feedback-form-group input,
1918
+ .feedback-modal.theme-dark .feedback-form-group textarea {
1919
+ background: #374151;
1920
+ border-color: #4B5563;
1921
+ color: white;
1922
+ }
1923
+
1924
+ .feedback-btn {
1925
+ position: relative;
1926
+ display: inline-flex;
1927
+ align-items: center;
1928
+ justify-content: center;
1929
+ overflow: hidden;
1930
+ border-radius: 6px;
1931
+ border: none;
1932
+ height: 40px;
1933
+ padding: 2px 16px;
1934
+ font-size: 14px;
1935
+ font-weight: 500;
1936
+ font-family: inherit;
1937
+ cursor: pointer;
1938
+ transition: all 0.2s ease;
1939
+ }
1940
+
1941
+ .feedback-btn:disabled {
1942
+ opacity: 0.7;
1943
+ cursor: not-allowed;
1944
+ }
1945
+
1946
+ .feedback-btn:focus-visible {
1947
+ outline: 2px solid #155EEF;
1948
+ outline-offset: 2px;
1949
+ }
1950
+
1951
+ .feedback-btn-submit {
1952
+ background: #155EEF;
1953
+ color: white;
1954
+ }
1955
+
1956
+ .feedback-btn-submit:hover:not(:disabled) {
1957
+ background: #004EEB;
1958
+ }
1959
+
1960
+ .feedback-btn-cancel {
1961
+ background: transparent;
1962
+ color: #6B7280;
1963
+ border: 1px solid #D1D5DB;
1964
+ }
1965
+
1966
+ .feedback-btn-cancel:hover:not(:disabled) {
1967
+ background: #F9FAFB;
1968
+ border-color: #9CA3AF;
1969
+ color: #374151;
1970
+ }
1971
+
1972
+ .feedback-modal.theme-dark .feedback-btn-cancel {
1973
+ color: #D1D5DB;
1974
+ border-color: #4B5563;
1975
+ }
1976
+
1977
+ .feedback-modal.theme-dark .feedback-btn-cancel:hover:not(:disabled) {
1978
+ background: #374151;
1979
+ }
1980
+
1981
+ .feedback-form-actions {
1982
+ display: flex;
1983
+ gap: 8px;
1984
+ justify-content: flex-end;
1985
+ margin-top: 16px;
1986
+ padding-top: 4px;
1987
+ }
1988
+
1989
+ .feedback-loading {
1990
+ width: 20px;
1991
+ height: 20px;
1992
+ border-radius: 50%;
1993
+ mask: radial-gradient(transparent 62%, white 65%);
1994
+ -webkit-mask: radial-gradient(transparent 62%, white 65%);
1995
+ animation: feedbackRotate 0.7s linear infinite;
1996
+ }
1997
+
1998
+ .feedback-loading-white {
1999
+ background: conic-gradient(from 0deg, rgba(255, 255, 255, 0.5), white);
2000
+ }
2001
+
2002
+ .feedback-loading-blue {
2003
+ background: conic-gradient(from 0deg, #004EEB, #eff4ff);
2004
+ }
2005
+
2006
+ @keyframes feedbackRotate {
2007
+ 0% { transform: rotate(0deg); }
2008
+ 100% { transform: rotate(360deg); }
2009
+ }
2010
+
2011
+ .feedback-error {
2012
+ color: #F04438;
2013
+ font-size: 14px;
2014
+ font-weight: 400;
2015
+ margin-top: 4px;
2016
+ text-transform: capitalize;
2017
+ }
2018
+
2019
+ .feedback-form-error {
2020
+ color: #F04438;
2021
+ font-size: 14px;
2022
+ margin-top: 12px;
2023
+ padding: 8px 12px;
2024
+ background: #FEE2E2;
2025
+ border: 1px solid #FECACA;
2026
+ border-radius: 6px;
2027
+ }
2028
+
2029
+ .feedback-modal.theme-dark .feedback-form-error {
2030
+ background: #7F1D1D;
2031
+ border-color: #991B1B;
2032
+ color: #FCA5A5;
2033
+ }
2034
+
2035
+ .feedback-form-group.error input,
2036
+ .feedback-form-group.error textarea {
2037
+ border-color: #FDA29B;
2038
+ }
2039
+
2040
+ .feedback-form-group.error input:focus,
2041
+ .feedback-form-group.error textarea:focus {
2042
+ border-color: #FDA29B;
2043
+ box-shadow: 0 0 0 1px rgba(16, 24, 40, 0.05), 0 0 0 4px rgba(253, 162, 155, 0.3);
2044
+ }
2045
+
2046
+ .feedback-success-notification {
2047
+ position: fixed;
2048
+ top: 20px;
2049
+ right: 20px;
2050
+ z-index: 1000001;
2051
+ background: white;
2052
+ border: 1px solid #D1FAE5;
2053
+ border-radius: 8px;
2054
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
2055
+ animation: slideInRight 0.3s ease-out;
2056
+ }
2057
+
2058
+ .feedback-success-content {
2059
+ display: flex;
2060
+ align-items: center;
2061
+ padding: 12px 16px;
2062
+ gap: 12px;
2063
+ }
2064
+
2065
+ .feedback-success-content span {
2066
+ color: #059669;
2067
+ font-weight: 500;
2068
+ font-size: 14px;
2069
+ }
2070
+
2071
+ .feedback-success-close {
2072
+ background: none;
2073
+ border: none;
2074
+ color: #6B7280;
2075
+ cursor: pointer;
2076
+ font-size: 18px;
2077
+ padding: 0;
2078
+ width: 20px;
2079
+ height: 20px;
2080
+ display: flex;
2081
+ align-items: center;
2082
+ justify-content: center;
2083
+ transition: all 0.3s ease;
2084
+ }
2085
+
2086
+ .feedback-success-close:hover {
2087
+ color: #374151;
2088
+ }
2089
+
2090
+ .feedback-success-close:focus-visible {
2091
+ outline: 2px solid #155EEF;
2092
+ outline-offset: 2px;
2093
+ }
2094
+
2095
+ @keyframes slideInRight {
2096
+ from {
2097
+ transform: translateX(100%);
2098
+ opacity: 0;
2099
+ }
2100
+ to {
2101
+ transform: translateX(0);
2102
+ opacity: 1;
2103
+ }
2104
+ }
2105
+
2106
+ @keyframes fadeIn {
2107
+ from { opacity: 0; }
2108
+ to { opacity: 1; }
2109
+ }
2110
+
2111
+ .feedback-modal {
2112
+ animation: fadeIn 0.2s ease-out;
2113
+ }
2114
+
2115
+ .feedback-modal-content {
2116
+ animation: slideInUp 0.3s ease-out;
2117
+ }
2118
+
2119
+ @keyframes slideInUp {
2120
+ from {
2121
+ transform: translateY(20px);
2122
+ opacity: 0;
2123
+ }
2124
+ to {
2125
+ transform: translateY(0);
2126
+ opacity: 1;
2127
+ }
2128
+ }
2129
+
2130
+ @media (max-width: 640px) {
2131
+ .feedback-modal {
2132
+ padding: 8px;
2133
+ }
2134
+
2135
+ .feedback-modal-content {
2136
+ min-width: 280px;
2137
+ max-width: 100%;
2138
+ max-height: 95vh;
2139
+ }
2140
+
2141
+ .feedback-form {
2142
+ padding: 16px;
2143
+ }
2144
+
2145
+ .feedback-modal-header {
2146
+ padding: 16px;
2147
+ }
2148
+
2149
+ .feedback-modal-header h3 {
2150
+ font-size: 15px;
2151
+ }
2152
+
2153
+ .feedback-form-actions {
2154
+ flex-direction: column;
2155
+ gap: 8px;
2156
+ }
2157
+
2158
+ .feedback-btn {
2159
+ width: 100%;
2160
+ height: 40px;
2161
+ }
2162
+
2163
+ .feedback-widget-button {
2164
+ bottom: 16px;
2165
+ right: 16px;
2166
+ }
2167
+
2168
+ .feedback-widget-button.position-bottom-left {
2169
+ left: 16px;
2170
+ }
2171
+
2172
+ .feedback-trigger-btn {
2173
+ padding: 10px 16px;
2174
+ font-size: 13px;
2175
+ }
2176
+
2177
+ .feedback-success-notification {
2178
+ top: 8px;
2179
+ right: 8px;
2180
+ left: 8px;
2181
+ max-width: none;
2182
+ }
2183
+
2184
+ .feedback-form-group input {
2185
+ height: 40px;
2186
+ padding: 2px 12px;
2187
+ }
2188
+
2189
+ .feedback-form-group textarea {
2190
+ min-height: 80px;
2191
+ padding: 2px 12px;
2192
+ }
2193
+ }
2194
+
2195
+ @media (prefers-reduced-motion: reduce) {
2196
+ .feedback-trigger-btn,
2197
+ .feedback-btn,
2198
+ .feedback-modal,
2199
+ .feedback-modal-content,
2200
+ .feedback-success-notification,
2201
+ .feedback-loading {
2202
+ transition: none;
2203
+ animation: none;
2204
+ }
2205
+
2206
+ .feedback-trigger-btn:hover {
2207
+ transform: none;
2208
+ }
2209
+ }
2210
+
2211
+ @media print {
2212
+ .feedback-widget,
2213
+ .feedback-modal,
2214
+ .feedback-success-notification {
2215
+ display: none !important;
2216
+ }
2217
+ }
2218
+ `;
2219
+
2220
+ function injectStyles() {
2221
+ console.log('injectStyles called');
2222
+ console.log('document exists:', typeof document !== 'undefined');
2223
+ console.log('CSS_STYLES exists:', true);
2224
+
2225
+ if (
2226
+ typeof document !== 'undefined' &&
2227
+ !document.querySelector('#feedback-sdk-styles')
2228
+ ) {
2229
+ console.log('Injecting CSS...');
2230
+ const style = document.createElement('style');
2231
+ style.id = 'feedback-sdk-styles';
2232
+ style.textContent = CSS_STYLES;
2233
+ document.head.appendChild(style);
2234
+ console.log(
2235
+ 'CSS injected, style element created:',
2236
+ !!document.querySelector('#feedback-sdk-styles')
2237
+ );
2238
+ } else {
2239
+ console.log('CSS already exists or document not ready');
2240
+ }
2241
+ }
2242
+
2243
+ function autoInit() {
2244
+ if (typeof window !== 'undefined' && window.FeedbackSDKConfig) {
2245
+ injectStyles();
2246
+
2247
+ const config = { ...window.FeedbackSDKConfig };
2248
+
2249
+ if (!config.userContext) {
2250
+ config.userContext = getUserContextFromEnvironment();
2251
+ }
2252
+
2253
+ const sdk = new FeedbackSDK(config);
2254
+
2255
+ sdk
2256
+ .init()
2257
+ .then((initData) => {
2258
+ window.FeedbackSDK.instance = sdk;
2259
+
2260
+ if (window.FeedbackSDKConfig.autoCreate) {
2261
+ const widgets = Array.isArray(window.FeedbackSDKConfig.autoCreate)
2262
+ ? window.FeedbackSDKConfig.autoCreate
2263
+ : [window.FeedbackSDKConfig.autoCreate];
2264
+
2265
+ widgets.forEach((widgetConfig) => {
2266
+ try {
2267
+ const widget = sdk.createWidget(
2268
+ widgetConfig.type || 'button',
2269
+ widgetConfig
2270
+ );
2271
+ widget.mount(widgetConfig.container);
2272
+ } catch (error) {
2273
+ console.error('[FeedbackSDK] Failed to create widget:', error);
2274
+ }
2275
+ });
2276
+ }
2277
+
2278
+ if (typeof CustomEvent !== 'undefined') {
2279
+ const event = new CustomEvent('FeedbackSDKReady', {
2280
+ detail: {
2281
+ sdk,
2282
+ config: config,
2283
+ initData: initData,
2284
+ },
2285
+ });
2286
+ window.dispatchEvent(event);
2287
+ }
2288
+
2289
+ console.log(
2290
+ '[FeedbackSDK] Successfully initialized with session:',
2291
+ initData.sessionToken ? 'Yes' : 'No'
2292
+ );
2293
+ })
2294
+ .catch((error) => {
2295
+ console.error('[FeedbackSDK] Auto-initialization failed:', error);
2296
+
2297
+ if (typeof CustomEvent !== 'undefined') {
2298
+ const event = new CustomEvent('FeedbackSDKError', {
2299
+ detail: {
2300
+ error,
2301
+ config: config,
2302
+ phase: 'initialization',
2303
+ },
2304
+ });
2305
+ window.dispatchEvent(event);
2306
+ }
2307
+ });
2308
+ }
2309
+ }
2310
+
2311
+ function getUserContextFromEnvironment() {
2312
+ if (typeof window === 'undefined') return null;
2313
+
2314
+ if (window.FeedbackSDKUserContext) {
2315
+ return window.FeedbackSDKUserContext;
2316
+ }
2317
+
2318
+ const authSources = [
2319
+ () => window.auth0?.user,
2320
+ () => window.firebase?.auth()?.currentUser,
2321
+ () => window.amplify?.Auth?.currentAuthenticatedUser(),
2322
+
2323
+ () => window.currentUser,
2324
+ () => window.user,
2325
+ () => window.userData,
2326
+
2327
+ () => window.app?.user,
2328
+ () => window.store?.getState?.()?.user,
2329
+ () => window.App?.currentUser,
2330
+ ];
2331
+
2332
+ for (const getAuth of authSources) {
2333
+ try {
2334
+ const authData = getAuth();
2335
+ if (authData) {
2336
+ const userContext = FeedbackSDK.extractUserContextFromAuth(authData);
2337
+ if (userContext && (userContext.user_id || userContext.email)) {
2338
+ console.log(
2339
+ '[FeedbackSDK] Auto-detected user context from',
2340
+ getAuth.name || 'unknown source'
2341
+ );
2342
+ return userContext;
2343
+ }
2344
+ }
2345
+ } catch (error) {
2346
+ continue;
2347
+ }
2348
+ }
2349
+
2350
+ try {
2351
+ const storedAuth =
2352
+ localStorage.getItem('auth') ||
2353
+ localStorage.getItem('user') ||
2354
+ localStorage.getItem('session');
2355
+ if (storedAuth) {
2356
+ const authData = JSON.parse(storedAuth);
2357
+ const userContext = FeedbackSDK.extractUserContextFromAuth(authData);
2358
+ if (userContext && (userContext.user_id || userContext.email)) {
2359
+ console.log(
2360
+ '[FeedbackSDK] Auto-detected user context from localStorage'
2361
+ );
2362
+ return userContext;
2363
+ }
2364
+ }
2365
+ } catch (error) {
2366
+ // Continue
2367
+ }
2368
+
2369
+ console.warn(
2370
+ '[FeedbackSDK] No user context found. Widget initialization may require manual user context setting.'
2371
+ );
2372
+ return null;
2373
+ }
2374
+
2375
+ function handleDOMReady() {
2376
+ if (typeof document !== 'undefined') {
2377
+ if (document.readyState === 'loading') {
2378
+ document.addEventListener('DOMContentLoaded', autoInit);
2379
+ } else {
2380
+ setTimeout(autoInit, 0);
2381
+ }
2382
+ }
2383
+ }
2384
+
2385
+ const FeedbackSDKExport = {
2386
+ FeedbackSDK,
2387
+ BaseWidget,
2388
+ ButtonWidget,
2389
+ TabWidget,
2390
+ InlineWidget,
2391
+ WidgetFactory,
2392
+ EventBus,
2393
+ APIService,
2394
+ SDKError,
2395
+ APIError,
2396
+ WidgetError,
2397
+ ConfigError,
2398
+ ValidationError,
2399
+ helpers,
2400
+ create: (config) => {
2401
+ injectStyles();
2402
+ return new FeedbackSDK(config);
2403
+ },
2404
+ version: '1.0.0',
2405
+ instance: null,
2406
+
2407
+ isReady: () => Boolean(FeedbackSDKExport.instance),
2408
+ getInstance: () => FeedbackSDKExport.instance,
2409
+
2410
+ setUserContext: (userContext) => {
2411
+ if (FeedbackSDKExport.instance) {
2412
+ FeedbackSDKExport.instance.setUserContext(userContext);
2413
+ } else {
2414
+ if (typeof window !== 'undefined') {
2415
+ window.FeedbackSDKUserContext = userContext;
2416
+ }
2417
+ }
2418
+ },
2419
+
2420
+ initWithUser: async (config, userContext) => {
2421
+ injectStyles();
2422
+ const fullConfig = { ...config, userContext };
2423
+ const sdk = new FeedbackSDK(fullConfig);
2424
+ await sdk.init();
2425
+
2426
+ if (typeof window !== 'undefined') {
2427
+ window.FeedbackSDK.instance = sdk;
2428
+ }
2429
+
2430
+ return sdk;
2431
+ },
2432
+
2433
+ onReady: (callback) => {
2434
+ if (typeof window !== 'undefined') {
2435
+ if (FeedbackSDKExport.isReady()) {
2436
+ callback(FeedbackSDKExport.instance);
2437
+ } else {
2438
+ window.addEventListener(
2439
+ 'FeedbackSDKReady',
2440
+ (event) => {
2441
+ callback(event.detail.sdk, event.detail);
2442
+ },
2443
+ { once: true }
2444
+ );
2445
+ }
2446
+ }
2447
+ },
2448
+
2449
+ onError: (callback) => {
2450
+ if (typeof window !== 'undefined') {
2451
+ window.addEventListener('FeedbackSDKError', (event) => {
2452
+ callback(event.detail.error, event.detail);
2453
+ });
2454
+ }
2455
+ },
2456
+
2457
+ extractUserContext: FeedbackSDK.extractUserContextFromAuth,
2458
+ };
2459
+
2460
+ if (typeof window !== 'undefined') {
2461
+ window.FeedbackSDK = FeedbackSDKExport;
2462
+ handleDOMReady();
2463
+ }
2464
+
2465
+ exports.APIError = APIError;
2466
+ exports.APIService = APIService;
2467
+ exports.BaseWidget = BaseWidget;
2468
+ exports.ButtonWidget = ButtonWidget;
2469
+ exports.ConfigError = ConfigError;
2470
+ exports.EventBus = EventBus;
2471
+ exports.FeedbackSDK = FeedbackSDK;
2472
+ exports.InlineWidget = InlineWidget;
2473
+ exports.SDKError = SDKError;
2474
+ exports.TabWidget = TabWidget;
2475
+ exports.ValidationError = ValidationError;
2476
+ exports.WidgetError = WidgetError;
2477
+ exports.WidgetFactory = WidgetFactory;
2478
+ exports.default = FeedbackSDKExport;
2479
+ exports.helpers = helpers;
2480
+
2481
+ Object.defineProperty(exports, '__esModule', { value: true });
2482
+
2483
+ }));