@product7/product7-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +1025 -0
  2. package/dist/README.md +1025 -0
  3. package/dist/product7-js.js +14658 -0
  4. package/dist/product7-js.js.map +1 -0
  5. package/dist/product7-js.min.js +2 -0
  6. package/dist/product7-js.min.js.map +1 -0
  7. package/package.json +114 -0
  8. package/src/api/mock-data/index.js +360 -0
  9. package/src/api/services/ChangelogService.js +28 -0
  10. package/src/api/services/FeedbackService.js +44 -0
  11. package/src/api/services/HelpService.js +50 -0
  12. package/src/api/services/MessengerService.js +279 -0
  13. package/src/api/services/SurveyService.js +127 -0
  14. package/src/api/utils/helpers.js +30 -0
  15. package/src/core/APIService.js +303 -0
  16. package/src/core/BaseAPIService.js +298 -0
  17. package/src/core/EventBus.js +54 -0
  18. package/src/core/Product7.js +812 -0
  19. package/src/core/WebSocketService.js +275 -0
  20. package/src/docs/api.md +226 -0
  21. package/src/docs/example.md +461 -0
  22. package/src/docs/framework-integrations.md +714 -0
  23. package/src/docs/installation.md +281 -0
  24. package/src/index.js +894 -0
  25. package/src/styles/base.js +50 -0
  26. package/src/styles/changelog.js +665 -0
  27. package/src/styles/components.js +553 -0
  28. package/src/styles/design-tokens.js +124 -0
  29. package/src/styles/feedback.js +325 -0
  30. package/src/styles/messenger-components.js +632 -0
  31. package/src/styles/messenger-core.js +233 -0
  32. package/src/styles/messenger-features.js +169 -0
  33. package/src/styles/messenger-views.js +877 -0
  34. package/src/styles/messenger.js +17 -0
  35. package/src/styles/messengerCustomStyles.js +114 -0
  36. package/src/styles/styles.js +26 -0
  37. package/src/styles/survey.js +894 -0
  38. package/src/utils/errors.js +142 -0
  39. package/src/utils/helpers.js +219 -0
  40. package/src/widgets/BaseWidget.js +548 -0
  41. package/src/widgets/ButtonWidget.js +104 -0
  42. package/src/widgets/ChangelogWidget.js +615 -0
  43. package/src/widgets/InlineWidget.js +148 -0
  44. package/src/widgets/MessengerWidget.js +979 -0
  45. package/src/widgets/SurveyWidget.js +1325 -0
  46. package/src/widgets/TabWidget.js +45 -0
  47. package/src/widgets/WidgetFactory.js +70 -0
  48. package/src/widgets/messenger/MessengerState.js +323 -0
  49. package/src/widgets/messenger/components/MessengerLauncher.js +124 -0
  50. package/src/widgets/messenger/components/MessengerPanel.js +111 -0
  51. package/src/widgets/messenger/components/NavigationTabs.js +130 -0
  52. package/src/widgets/messenger/views/ChangelogView.js +167 -0
  53. package/src/widgets/messenger/views/ChatView.js +592 -0
  54. package/src/widgets/messenger/views/ConversationsView.js +244 -0
  55. package/src/widgets/messenger/views/HelpView.js +239 -0
  56. package/src/widgets/messenger/views/HomeView.js +300 -0
  57. package/src/widgets/messenger/views/PreChatFormView.js +109 -0
  58. package/types/index.d.ts +341 -0
package/src/index.js ADDED
@@ -0,0 +1,894 @@
1
+ import { APIService } from './core/APIService.js';
2
+ import { EventBus } from './core/EventBus.js';
3
+ import { Product7 as Product7SDK } from './core/Product7.js';
4
+ import { CSS_STYLES } from './styles/styles.js';
5
+ import {
6
+ APIError,
7
+ ConfigError,
8
+ SDKError,
9
+ ValidationError,
10
+ WidgetError,
11
+ } from './utils/errors.js';
12
+ import * as helpers from './utils/helpers.js';
13
+ import { BaseWidget } from './widgets/BaseWidget.js';
14
+ import { ButtonWidget } from './widgets/ButtonWidget.js';
15
+ import { ChangelogWidget } from './widgets/ChangelogWidget.js';
16
+ import { InlineWidget } from './widgets/InlineWidget.js';
17
+ import { MessengerWidget } from './widgets/MessengerWidget.js';
18
+ import { SurveyWidget } from './widgets/SurveyWidget.js';
19
+ import { TabWidget } from './widgets/TabWidget.js';
20
+ import { WidgetFactory } from './widgets/WidgetFactory.js';
21
+
22
+ function injectStyles() {
23
+ if (typeof document === 'undefined') return;
24
+
25
+ if (!document.querySelector('#product7-styles')) {
26
+ const style = document.createElement('style');
27
+ style.id = 'product7-styles';
28
+ style.textContent = CSS_STYLES;
29
+ document.head.appendChild(style);
30
+ }
31
+
32
+ if (!document.querySelector('#product7-iconify')) {
33
+ const script = document.createElement('script');
34
+ script.id = 'product7-iconify';
35
+ script.src =
36
+ 'https://cdn.jsdelivr.net/npm/iconify-icon@2/dist/iconify-icon.min.js';
37
+ script.async = true;
38
+ document.head.appendChild(script);
39
+ }
40
+ }
41
+
42
+ // --- Identify: transform flat user data into internal format ---
43
+
44
+ function transformIdentifyData(data) {
45
+ const {
46
+ organization,
47
+ email,
48
+ name,
49
+ userId,
50
+ profilePicture,
51
+ locale,
52
+ companies,
53
+ ...customAttrs
54
+ } = data;
55
+
56
+ const metadata = {
57
+ user_id: userId,
58
+ email,
59
+ name,
60
+ profile_picture: profilePicture,
61
+ };
62
+
63
+ // Flat custom attributes → custom_fields
64
+ const knownKeys = [
65
+ 'organization',
66
+ 'email',
67
+ 'name',
68
+ 'userId',
69
+ 'profilePicture',
70
+ 'locale',
71
+ 'companies',
72
+ 'debug',
73
+ 'mock',
74
+ 'env',
75
+ 'apiUrl',
76
+ ];
77
+ const customFields = {};
78
+ for (const [key, value] of Object.entries(customAttrs)) {
79
+ if (!knownKeys.includes(key)) {
80
+ customFields[key] = value;
81
+ }
82
+ }
83
+ if (Object.keys(customFields).length > 0) {
84
+ metadata.custom_fields = customFields;
85
+ }
86
+
87
+ // Companies array → company (use first for now, backend supports single)
88
+ if (Array.isArray(companies) && companies.length > 0) {
89
+ const c = companies[0];
90
+ metadata.company = {
91
+ id: c.id,
92
+ name: c.name,
93
+ monthly_spend: c.monthlySpend,
94
+ custom_fields: {},
95
+ };
96
+ const companyKnownKeys = ['id', 'name', 'monthlySpend', 'createdAt'];
97
+ for (const [key, value] of Object.entries(c)) {
98
+ if (!companyKnownKeys.includes(key)) {
99
+ metadata.company.custom_fields[key] = value;
100
+ }
101
+ }
102
+ }
103
+
104
+ return {
105
+ workspace: organization,
106
+ metadata,
107
+ locale,
108
+ debug: data.debug,
109
+ mock: data.mock,
110
+ env: data.env,
111
+ apiUrl: data.apiUrl,
112
+ };
113
+ }
114
+
115
+ function cleanUndefined(obj) {
116
+ Object.keys(obj).forEach((key) => {
117
+ if (obj[key] === undefined) delete obj[key];
118
+ });
119
+ return obj;
120
+ }
121
+
122
+ // --- Ensure SDK is initialized (shared by widget inits) ---
123
+
124
+ async function ensureSDK(options) {
125
+ if (Product7._sdk) return Product7._sdk;
126
+
127
+ if (options.organization) {
128
+ injectStyles();
129
+ const config = cleanUndefined({
130
+ workspace: options.organization,
131
+ debug: options.debug,
132
+ mock: options.mock,
133
+ env: options.env,
134
+ apiUrl: options.apiUrl,
135
+ });
136
+
137
+ const sdk = new Product7SDK(config);
138
+ await sdk.init();
139
+ Product7._sdk = sdk;
140
+ Product7._organization = options.organization;
141
+ return sdk;
142
+ }
143
+
144
+ return null;
145
+ }
146
+
147
+ // --- Public API ---
148
+
149
+ const Product7 = {
150
+ // Internal state
151
+ _sdk: null,
152
+ _identified: false,
153
+ _organization: null,
154
+ _q: [],
155
+ _feedbackWidget: null,
156
+ _messengerWidget: null,
157
+ _surveyWidget: null,
158
+
159
+ version: '1.0.0',
160
+
161
+ // ==================
162
+ // Identify
163
+ // ==================
164
+
165
+ async identify(data = {}, callback) {
166
+ try {
167
+ injectStyles();
168
+
169
+ const transformed = transformIdentifyData(data);
170
+ Product7._organization = transformed.workspace;
171
+
172
+ const config = cleanUndefined({
173
+ workspace: transformed.workspace,
174
+ metadata: transformed.metadata,
175
+ debug: transformed.debug,
176
+ mock: transformed.mock,
177
+ env: transformed.env,
178
+ apiUrl: transformed.apiUrl,
179
+ });
180
+
181
+ const sdk = new Product7SDK(config);
182
+ const initData = await sdk.init();
183
+
184
+ Product7._sdk = sdk;
185
+ Product7._identified = true;
186
+
187
+ // Sync custom attributes + segments via /widget/identify
188
+ if (transformed.metadata && sdk.apiService?.sessionToken) {
189
+ try {
190
+ const identifyPayload = {
191
+ user_id: transformed.metadata.user_id,
192
+ email: transformed.metadata.email,
193
+ name: transformed.metadata.name,
194
+ avatar: transformed.metadata.profile_picture,
195
+ attributes: transformed.metadata.custom_fields || {},
196
+ };
197
+ if (transformed.metadata.company) {
198
+ identifyPayload.company = transformed.metadata.company;
199
+ }
200
+ await sdk.apiService._makeRequest('/widget/identify', {
201
+ method: 'POST',
202
+ body: JSON.stringify(identifyPayload),
203
+ headers: {
204
+ 'Content-Type': 'application/json',
205
+ Authorization: `Bearer ${sdk.apiService.sessionToken}`,
206
+ },
207
+ });
208
+ } catch (identifyErr) {
209
+ if (config.debug) {
210
+ console.warn('[Product7] Attribute sync failed:', identifyErr);
211
+ }
212
+ }
213
+ }
214
+
215
+ Product7._flushQueue();
216
+
217
+ if (typeof window !== 'undefined' && typeof CustomEvent !== 'undefined') {
218
+ window.dispatchEvent(
219
+ new CustomEvent('Product7Ready', {
220
+ detail: { sdk, config, initData },
221
+ })
222
+ );
223
+ }
224
+
225
+ if (typeof callback === 'function') callback(null);
226
+ return initData;
227
+ } catch (error) {
228
+ console.error('[Product7] Identify failed:', error);
229
+
230
+ if (typeof window !== 'undefined' && typeof CustomEvent !== 'undefined') {
231
+ window.dispatchEvent(
232
+ new CustomEvent('Product7Error', {
233
+ detail: { error, phase: 'identify' },
234
+ })
235
+ );
236
+ }
237
+
238
+ if (typeof callback === 'function') callback(error);
239
+ throw error;
240
+ }
241
+ },
242
+
243
+ // ==================
244
+ // Feedback
245
+ // ==================
246
+
247
+ async feedback(options = {}, callback) {
248
+ const sdk = await ensureSDK(options);
249
+ if (!sdk) {
250
+ Product7._q.push(['feedback', options, callback]);
251
+ return null;
252
+ }
253
+
254
+ injectStyles();
255
+
256
+ const widgetOptions = cleanUndefined({
257
+ displayMode: options.displayMode || 'panel',
258
+ theme: options.theme || sdk.config.theme || 'light',
259
+ size: options.size || 'medium',
260
+ primaryColor: options.primaryColor,
261
+ backgroundColor: options.backgroundColor,
262
+ textColor: options.textColor,
263
+ suppressAfterSubmission: options.suppressAfterSubmission,
264
+ suppressionDays: options.suppressionDays,
265
+ showBackdrop: options.showBackdrop !== false,
266
+ metadata: options.metadata,
267
+ });
268
+
269
+ // placement → position
270
+ if (options.placement === 'right') {
271
+ widgetOptions.position = 'right';
272
+ } else if (options.placement === 'left') {
273
+ widgetOptions.position = 'left';
274
+ } else if (options.placement) {
275
+ widgetOptions.position = options.placement;
276
+ }
277
+
278
+ // defaultBoard → boardName
279
+ if (options.defaultBoard) {
280
+ widgetOptions.boardName = options.defaultBoard;
281
+ }
282
+
283
+ // No placement = modal mode, no floating button
284
+ if (!options.placement) {
285
+ widgetOptions.autoShow = false;
286
+ widgetOptions.displayMode = 'modal';
287
+ widgetOptions._noTriggerButton = true;
288
+ } else {
289
+ // Trigger button is always visible when placement is set
290
+ widgetOptions.suppressAfterSubmission = false;
291
+ widgetOptions.enabled = true;
292
+ }
293
+
294
+ try {
295
+ const widget = sdk.createWidget('button', widgetOptions);
296
+ widget.mount();
297
+
298
+ if (options.placement) {
299
+ widget.show();
300
+ } else {
301
+ widget.hide();
302
+ }
303
+
304
+ Product7._feedbackWidget = widget;
305
+
306
+ if (typeof callback === 'function') {
307
+ callback({ action: 'widgetReady' });
308
+
309
+ sdk.eventBus.on('feedback:submitted', (data) => {
310
+ callback({ action: 'feedbackSubmitted', post: data.feedback });
311
+ });
312
+
313
+ sdk.eventBus.on('feedback:error', (data) => {
314
+ callback({ action: 'feedbackError', error: data.error });
315
+ });
316
+ }
317
+
318
+ Product7._setupAttributeTriggers();
319
+ Product7._setupPostMessageListener();
320
+
321
+ return widget;
322
+ } catch (error) {
323
+ console.error('[Product7] Failed to initialize feedback widget:', error);
324
+ if (typeof callback === 'function') callback({ action: 'error', error });
325
+ return null;
326
+ }
327
+ },
328
+
329
+ openFeedback(options = {}) {
330
+ if (Product7._feedbackWidget) {
331
+ if (options.setBoard) {
332
+ Product7._feedbackWidget.options.boardName = options.setBoard;
333
+ }
334
+ Product7._feedbackWidget.openPanel();
335
+ }
336
+ },
337
+
338
+ closeFeedback() {
339
+ if (Product7._feedbackWidget) {
340
+ Product7._feedbackWidget.closePanel();
341
+ }
342
+ },
343
+
344
+ destroyFeedback() {
345
+ if (Product7._feedbackWidget) {
346
+ Product7._feedbackWidget.destroy();
347
+ Product7._feedbackWidget = null;
348
+ }
349
+ },
350
+
351
+ // ==================
352
+ // Messenger
353
+ // ==================
354
+
355
+ async messenger(options = {}, callback) {
356
+ const sdk = await ensureSDK(options);
357
+ if (!sdk) {
358
+ Product7._q.push(['messenger', options, callback]);
359
+ return null;
360
+ }
361
+
362
+ injectStyles();
363
+
364
+ const widgetOptions = cleanUndefined({
365
+ theme: options.theme || sdk.config.theme || 'light',
366
+ primaryColor: options.primaryColor,
367
+ backgroundColor: options.backgroundColor,
368
+ textColor: options.textColor,
369
+ logoUrl: options.logoUrl,
370
+ teamName: options.teamName,
371
+ teamAvatars: options.teamAvatars,
372
+ greetingMessage: options.greetingMessage,
373
+ welcomeMessage: options.welcomeMessage,
374
+ enableHelp: options.enableHelp,
375
+ enableChangelog: options.enableChangelog,
376
+ enableNews: options.enableNews,
377
+ autoLoadData: options.autoLoadData,
378
+ initialView: options.initialView,
379
+ featuredContent: options.featuredContent,
380
+ feedbackUrl: options.feedbackUrl,
381
+ changelogUrl: options.changelogUrl,
382
+ helpUrl: options.helpUrl,
383
+ roadmapUrl: options.roadmapUrl,
384
+ previewData: options.previewData,
385
+ });
386
+
387
+ // placement → position
388
+ if (options.placement === 'right') {
389
+ widgetOptions.position = 'right';
390
+ } else if (options.placement === 'left') {
391
+ widgetOptions.position = 'left';
392
+ } else if (options.placement) {
393
+ widgetOptions.position = options.placement;
394
+ }
395
+
396
+ // Messenger is always explicitly shown — never suppress based on feedback submission history
397
+ widgetOptions.suppressAfterSubmission = false;
398
+ widgetOptions.enabled = true;
399
+
400
+ try {
401
+ const widget = sdk.createWidget('messenger', widgetOptions);
402
+ widget.mount();
403
+ widget.show();
404
+
405
+ Product7._messengerWidget = widget;
406
+
407
+ if (typeof callback === 'function') {
408
+ callback({ action: 'widgetReady' });
409
+
410
+ sdk.eventBus.on('messenger:messageSent', (data) => {
411
+ callback({ action: 'messageSent', message: data });
412
+ });
413
+
414
+ sdk.eventBus.on('messenger:opened', () => {
415
+ callback({ action: 'messengerOpened' });
416
+ });
417
+
418
+ sdk.eventBus.on('messenger:closed', () => {
419
+ callback({ action: 'messengerClosed' });
420
+ });
421
+ }
422
+
423
+ // Listen for data-product7-messenger clicks
424
+ Product7._setupMessengerAttributeTriggers();
425
+ Product7._setupPostMessageListener();
426
+
427
+ return widget;
428
+ } catch (error) {
429
+ console.error('[Product7] Failed to initialize messenger widget:', error);
430
+ if (typeof callback === 'function') callback({ action: 'error', error });
431
+ return null;
432
+ }
433
+ },
434
+
435
+ openMessenger() {
436
+ if (Product7._messengerWidget) {
437
+ Product7._messengerWidget.open?.() ||
438
+ Product7._messengerWidget.openPanel?.();
439
+ }
440
+ },
441
+
442
+ closeMessenger() {
443
+ if (Product7._messengerWidget) {
444
+ Product7._messengerWidget.close?.() ||
445
+ Product7._messengerWidget.closePanel?.();
446
+ }
447
+ },
448
+
449
+ destroyMessenger() {
450
+ if (Product7._messengerWidget) {
451
+ Product7._messengerWidget.destroy();
452
+ Product7._messengerWidget = null;
453
+ }
454
+ },
455
+
456
+ // ==================
457
+ // Survey
458
+ // ==================
459
+
460
+ async survey(options = {}, callback) {
461
+ const sdk = await ensureSDK(options);
462
+ if (!sdk) {
463
+ Product7._q.push(['survey', options, callback]);
464
+ return null;
465
+ }
466
+
467
+ injectStyles();
468
+
469
+ // placement → position
470
+ let position = 'right';
471
+ if (options.placement === 'right') {
472
+ position = 'right';
473
+ } else if (options.placement === 'left') {
474
+ position = 'left';
475
+ } else if (options.placement === 'center') {
476
+ position = 'center';
477
+ } else if (options.placement === 'bottom') {
478
+ position = 'bottom';
479
+ } else if (options.placement) {
480
+ position = options.placement;
481
+ }
482
+
483
+ // displayMode: "popover" (default), "modal", "fullscreen"
484
+ const displayMode = options.displayMode || 'popover';
485
+ if (displayMode === 'modal' || displayMode === 'fullscreen') {
486
+ position = 'center';
487
+ }
488
+
489
+ // Wire up all lifecycle callbacks
490
+ const surveyCallbacks = {
491
+ onSubmit: (response) => {
492
+ if (typeof callback === 'function') {
493
+ callback({ action: 'surveySubmitted', response });
494
+ }
495
+ },
496
+ onDismiss: () => {
497
+ if (typeof callback === 'function') {
498
+ callback({ action: 'surveyDismissed' });
499
+ }
500
+ },
501
+ };
502
+
503
+ // Register event bus listeners for additional callbacks
504
+ if (typeof callback === 'function') {
505
+ sdk.eventBus.on('survey:shown', (data) => {
506
+ callback({ action: 'surveyShown', survey: data });
507
+ });
508
+
509
+ sdk.eventBus.on('survey:questionAnswered', (data) => {
510
+ callback({
511
+ action: 'questionAnswered',
512
+ pageId: data.pageId,
513
+ pageType: data.pageType,
514
+ answer: data.answer,
515
+ });
516
+ });
517
+
518
+ sdk.eventBus.on('survey:closed', () => {
519
+ callback({ action: 'surveyClosed' });
520
+ });
521
+ }
522
+
523
+ // Display method: delayed, exitIntent, or immediate (default)
524
+ const displayMethod = options.displayMethod || 'immediate';
525
+ const showSurvey = async () => {
526
+ try {
527
+ let widget;
528
+
529
+ if (options.surveyId) {
530
+ // Show specific survey by ID
531
+ const showOpts = {
532
+ position,
533
+ displayMode,
534
+ theme: options.theme || sdk.config.theme || 'light',
535
+ showBackdrop:
536
+ displayMode === 'modal' || displayMode === 'fullscreen',
537
+ ...surveyCallbacks,
538
+ };
539
+
540
+ // force flag: bypass eligibility checks
541
+ if (options.force) {
542
+ showOpts.context = { includeIneligible: true };
543
+ }
544
+
545
+ widget = await sdk.showSurveyById(options.surveyId, showOpts);
546
+ } else {
547
+ // Auto-fetch and show first eligible survey
548
+ const fetchOpts = options.force ? { includeIneligible: true } : {};
549
+ const surveys = await sdk.getActiveSurveys(fetchOpts);
550
+
551
+ if (surveys.length === 0) {
552
+ if (typeof callback === 'function') {
553
+ callback({ action: 'noSurveys' });
554
+ }
555
+ return null;
556
+ }
557
+
558
+ const surveyConfig = surveys[0];
559
+ widget = sdk.showSurvey({
560
+ surveyId: surveyConfig.surveyId || surveyConfig.id,
561
+ surveyType: surveyConfig.surveyType || surveyConfig.type || 'nps',
562
+ position,
563
+ displayMode,
564
+ theme: options.theme || sdk.config.theme || 'light',
565
+ showBackdrop:
566
+ displayMode === 'modal' || displayMode === 'fullscreen',
567
+ title: surveyConfig.title,
568
+ description: surveyConfig.description,
569
+ lowLabel: surveyConfig.lowLabel,
570
+ highLabel: surveyConfig.highLabel,
571
+ ratingScale: surveyConfig.ratingScale,
572
+ showFeedbackInput: surveyConfig.showFeedbackInput,
573
+ showSubmitButton: surveyConfig.showSubmitButton,
574
+ autoSubmitOnSelect: surveyConfig.autoSubmitOnSelect,
575
+ showTitle: surveyConfig.showTitle,
576
+ showDescription: surveyConfig.showDescription,
577
+ customQuestions: surveyConfig.customQuestions,
578
+ pages: surveyConfig.pages,
579
+ enabled: options.force ? true : surveyConfig.enabled,
580
+ ...surveyCallbacks,
581
+ });
582
+
583
+ if (typeof callback === 'function') {
584
+ callback({ action: 'surveyReady', survey: surveyConfig });
585
+ }
586
+ }
587
+
588
+ Product7._surveyWidget = widget;
589
+ return widget;
590
+ } catch (error) {
591
+ console.error('[Product7] Failed to show survey:', error);
592
+ if (typeof callback === 'function')
593
+ callback({ action: 'error', error });
594
+ return null;
595
+ }
596
+ };
597
+
598
+ // Handle display methods
599
+ if (displayMethod === 'delayed') {
600
+ const delay = options.displayDelay || 5000;
601
+ Product7._surveyTimer = setTimeout(showSurvey, delay);
602
+ if (typeof callback === 'function') {
603
+ callback({ action: 'surveyScheduled', delay });
604
+ }
605
+ return null;
606
+ }
607
+
608
+ if (displayMethod === 'exitIntent') {
609
+ const handler = (e) => {
610
+ if (e.clientY <= 0) {
611
+ document.removeEventListener('mouseout', handler);
612
+ Product7._exitIntentHandler = null;
613
+ showSurvey();
614
+ }
615
+ };
616
+ Product7._exitIntentHandler = handler;
617
+ document.addEventListener('mouseout', handler);
618
+ if (typeof callback === 'function') {
619
+ callback({ action: 'surveyScheduled', trigger: 'exitIntent' });
620
+ }
621
+ return null;
622
+ }
623
+
624
+ if (displayMethod === 'onScroll') {
625
+ const scrollPercent = options.scrollPercentage || 50;
626
+ const handler = () => {
627
+ const scrolled =
628
+ (window.scrollY / (document.body.scrollHeight - window.innerHeight)) *
629
+ 100;
630
+ if (scrolled >= scrollPercent) {
631
+ window.removeEventListener('scroll', handler);
632
+ Product7._scrollHandler = null;
633
+ showSurvey();
634
+ }
635
+ };
636
+ Product7._scrollHandler = handler;
637
+ window.addEventListener('scroll', handler, { passive: true });
638
+ if (typeof callback === 'function') {
639
+ callback({
640
+ action: 'surveyScheduled',
641
+ trigger: 'onScroll',
642
+ scrollPercentage: scrollPercent,
643
+ });
644
+ }
645
+ return null;
646
+ }
647
+
648
+ // Default: immediate
649
+ return showSurvey();
650
+ },
651
+
652
+ closeSurvey() {
653
+ if (Product7._surveyWidget) {
654
+ Product7._surveyWidget.hide?.() || Product7._surveyWidget.closePanel?.();
655
+ }
656
+ },
657
+
658
+ destroySurvey() {
659
+ // Clean up scheduled triggers
660
+ if (Product7._surveyTimer) {
661
+ clearTimeout(Product7._surveyTimer);
662
+ Product7._surveyTimer = null;
663
+ }
664
+ if (Product7._exitIntentHandler) {
665
+ document.removeEventListener('mouseout', Product7._exitIntentHandler);
666
+ Product7._exitIntentHandler = null;
667
+ }
668
+ if (Product7._scrollHandler) {
669
+ window.removeEventListener('scroll', Product7._scrollHandler);
670
+ Product7._scrollHandler = null;
671
+ }
672
+ if (Product7._surveyWidget) {
673
+ Product7._surveyWidget.destroy();
674
+ Product7._surveyWidget = null;
675
+ }
676
+ },
677
+
678
+ // ==================
679
+ // Event Tracking
680
+ // ==================
681
+
682
+ async track(eventName, properties = {}) {
683
+ if (!Product7._sdk) {
684
+ Product7._q.push(['track', eventName, properties]);
685
+ return null;
686
+ }
687
+
688
+ const sdk = Product7._sdk;
689
+ if (!sdk.apiService?.sessionToken) return null;
690
+
691
+ try {
692
+ const result = await sdk.apiService._makeRequest('/widget/track', {
693
+ method: 'POST',
694
+ body: JSON.stringify({
695
+ event: eventName,
696
+ properties,
697
+ timestamp: new Date().toISOString(),
698
+ }),
699
+ headers: {
700
+ 'Content-Type': 'application/json',
701
+ Authorization: `Bearer ${sdk.apiService.sessionToken}`,
702
+ },
703
+ });
704
+
705
+ // If server returns a survey to show, show it
706
+ if (result?.survey) {
707
+ Product7.survey({
708
+ surveyId: result.survey.id || result.survey.survey_id,
709
+ force: true,
710
+ });
711
+ }
712
+
713
+ return result;
714
+ } catch (error) {
715
+ if (sdk.config.debug) {
716
+ console.warn('[Product7] Track failed:', error);
717
+ }
718
+ return null;
719
+ }
720
+ },
721
+
722
+ // ==================
723
+ // General
724
+ // ==================
725
+
726
+ getInstance() {
727
+ return Product7._sdk;
728
+ },
729
+
730
+ isReady() {
731
+ return Boolean(Product7._sdk?.initialized);
732
+ },
733
+
734
+ onReady(callback) {
735
+ if (typeof window === 'undefined') return;
736
+
737
+ if (Product7.isReady()) {
738
+ callback(Product7._sdk);
739
+ } else {
740
+ window.addEventListener(
741
+ 'Product7Ready',
742
+ (event) => callback(event.detail.sdk, event.detail),
743
+ { once: true }
744
+ );
745
+ }
746
+ },
747
+
748
+ onError(callback) {
749
+ if (typeof window === 'undefined') return;
750
+
751
+ window.addEventListener('Product7Error', (event) => {
752
+ callback(event.detail.error, event.detail);
753
+ });
754
+ },
755
+
756
+ destroy() {
757
+ Product7.destroyFeedback();
758
+ Product7.destroyMessenger();
759
+ Product7.destroySurvey();
760
+ if (Product7._sdk) {
761
+ Product7._sdk.destroy();
762
+ Product7._sdk = null;
763
+ }
764
+ Product7._identified = false;
765
+ Product7._organization = null;
766
+ Product7._q = [];
767
+ Product7._feedbackListenerAttached = false;
768
+ Product7._messengerListenerAttached = false;
769
+ Product7._postMessageListenerAttached = false;
770
+ },
771
+
772
+ // ==================
773
+ // Internal
774
+ // ==================
775
+
776
+ _flushQueue() {
777
+ const queue = Product7._q.splice(0);
778
+ for (const [method, ...args] of queue) {
779
+ if (typeof Product7[method] === 'function') {
780
+ Product7[method](...args);
781
+ }
782
+ }
783
+ },
784
+
785
+ _setupAttributeTriggers() {
786
+ if (typeof document === 'undefined') return;
787
+ if (Product7._feedbackListenerAttached) return;
788
+ Product7._feedbackListenerAttached = true;
789
+
790
+ document.addEventListener('click', (e) => {
791
+ const trigger = e.target.closest('[data-product7-feedback]');
792
+ if (trigger) {
793
+ e.preventDefault();
794
+ Product7.openFeedback();
795
+ }
796
+ });
797
+ },
798
+
799
+ _setupMessengerAttributeTriggers() {
800
+ if (typeof document === 'undefined') return;
801
+ if (Product7._messengerListenerAttached) return;
802
+ Product7._messengerListenerAttached = true;
803
+
804
+ document.addEventListener('click', (e) => {
805
+ const trigger = e.target.closest('[data-product7-messenger]');
806
+ if (trigger) {
807
+ e.preventDefault();
808
+ Product7.openMessenger();
809
+ }
810
+ });
811
+ },
812
+
813
+ _setupPostMessageListener() {
814
+ if (typeof window === 'undefined') return;
815
+ if (Product7._postMessageListenerAttached) return;
816
+ Product7._postMessageListenerAttached = true;
817
+
818
+ window.addEventListener('message', (event) => {
819
+ if (event.data?.target !== 'Product7Widget') return;
820
+
821
+ const { action, setBoard } = event.data.data || {};
822
+ switch (action) {
823
+ case 'openFeedback':
824
+ Product7.openFeedback({ setBoard });
825
+ break;
826
+ case 'closeFeedback':
827
+ Product7.closeFeedback();
828
+ break;
829
+ case 'openMessenger':
830
+ Product7.openMessenger();
831
+ break;
832
+ case 'closeMessenger':
833
+ Product7.closeMessenger();
834
+ break;
835
+ case 'closeSurvey':
836
+ Product7.closeSurvey();
837
+ break;
838
+ }
839
+ });
840
+ },
841
+
842
+ // Expose internals for advanced/npm usage
843
+ Product7SDK,
844
+ BaseWidget,
845
+ ButtonWidget,
846
+ ChangelogWidget,
847
+ TabWidget,
848
+ InlineWidget,
849
+ SurveyWidget,
850
+ MessengerWidget,
851
+ WidgetFactory,
852
+ EventBus,
853
+ APIService,
854
+ SDKError,
855
+ APIError,
856
+ WidgetError,
857
+ ConfigError,
858
+ ValidationError,
859
+ helpers,
860
+ };
861
+
862
+ // --- Pre-load queue support ---
863
+ if (typeof window !== 'undefined') {
864
+ const existingQueue = window.Product7?._q || [];
865
+ window.Product7 = Product7;
866
+
867
+ for (const [method, ...args] of existingQueue) {
868
+ if (typeof Product7[method] === 'function') {
869
+ Product7[method](...args);
870
+ }
871
+ }
872
+ }
873
+
874
+ export default Product7;
875
+
876
+ export {
877
+ APIError,
878
+ APIService,
879
+ BaseWidget,
880
+ ButtonWidget,
881
+ ChangelogWidget,
882
+ ConfigError,
883
+ EventBus,
884
+ helpers,
885
+ InlineWidget,
886
+ MessengerWidget,
887
+ Product7SDK as Product7,
888
+ SDKError,
889
+ SurveyWidget,
890
+ TabWidget,
891
+ ValidationError,
892
+ WidgetError,
893
+ WidgetFactory,
894
+ };