@product7/feedback-sdk 1.2.5 → 1.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@product7/feedback-sdk",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
4
  "description": "JavaScript SDK for integrating Product7 feedback widgets into any website",
5
5
  "main": "dist/feedback-sdk.js",
6
6
  "module": "src/index.js",
@@ -60,6 +60,124 @@ const MOCK_CHANGELOGS = [
60
60
  },
61
61
  ];
62
62
 
63
+ // Mock conversations for development
64
+ const MOCK_CONVERSATIONS = [
65
+ {
66
+ id: 'conv_1',
67
+ subject: 'Question about pricing',
68
+ status: 'open',
69
+ last_message_at: new Date(Date.now() - 49 * 60 * 1000).toISOString(),
70
+ created_at: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
71
+ unread: 1,
72
+ assigned_user: {
73
+ id: 'user_1',
74
+ name: 'Sarah',
75
+ avatar: null,
76
+ },
77
+ },
78
+ {
79
+ id: 'conv_2',
80
+ subject: 'Feature request',
81
+ status: 'open',
82
+ last_message_at: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000).toISOString(),
83
+ created_at: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(),
84
+ unread: 0,
85
+ assigned_user: {
86
+ id: 'user_2',
87
+ name: 'Tom',
88
+ avatar: null,
89
+ },
90
+ },
91
+ ];
92
+
93
+ // Mock messages for development
94
+ const MOCK_MESSAGES = {
95
+ conv_1: [
96
+ {
97
+ id: 'msg_1',
98
+ content: "Hi there! 👋 I'm Sarah. How can I help you today?",
99
+ sender_type: 'agent',
100
+ sender_name: 'Sarah',
101
+ sender_avatar: null,
102
+ created_at: new Date(Date.now() - 50 * 60 * 1000).toISOString(),
103
+ },
104
+ {
105
+ id: 'msg_2',
106
+ content: 'Hi! I have a question about your enterprise pricing.',
107
+ sender_type: 'customer',
108
+ created_at: new Date(Date.now() - 49 * 60 * 1000).toISOString(),
109
+ },
110
+ ],
111
+ conv_2: [
112
+ {
113
+ id: 'msg_3',
114
+ content: "Hello! I'm Tom from the product team.",
115
+ sender_type: 'agent',
116
+ sender_name: 'Tom',
117
+ sender_avatar: null,
118
+ created_at: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
119
+ },
120
+ {
121
+ id: 'msg_4',
122
+ content: 'I would love to see a dark mode feature!',
123
+ sender_type: 'customer',
124
+ created_at: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000 - 30 * 60 * 1000).toISOString(),
125
+ },
126
+ {
127
+ id: 'msg_5',
128
+ content: "Great suggestion! That feature will be available next week. I'll let you know when it's ready.",
129
+ sender_type: 'agent',
130
+ sender_name: 'Tom',
131
+ sender_avatar: null,
132
+ created_at: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000).toISOString(),
133
+ },
134
+ ],
135
+ };
136
+
137
+ // Mock help collections for development
138
+ const MOCK_HELP_COLLECTIONS = [
139
+ {
140
+ id: 'collection_1',
141
+ title: 'Product Overview',
142
+ description: 'See how your AI-first customer service solution works.',
143
+ articleCount: 24,
144
+ icon: 'ph-book-open',
145
+ url: '#',
146
+ },
147
+ {
148
+ id: 'collection_2',
149
+ title: 'Getting Started',
150
+ description: 'Everything you need to know to get started with Product7.',
151
+ articleCount: 30,
152
+ icon: 'ph-rocket',
153
+ url: '#',
154
+ },
155
+ {
156
+ id: 'collection_3',
157
+ title: 'AI Agent',
158
+ description: 'Resolving customer questions instantly and accurately—from live chat to email.',
159
+ articleCount: 82,
160
+ icon: 'ph-robot',
161
+ url: '#',
162
+ },
163
+ {
164
+ id: 'collection_4',
165
+ title: 'Channels',
166
+ description: 'Enabling the channels you use to communicate with customers, all from the Inbox.',
167
+ articleCount: 45,
168
+ icon: 'ph-chat-circle',
169
+ url: '#',
170
+ },
171
+ {
172
+ id: 'collection_5',
173
+ title: 'Billing & Payments',
174
+ description: 'Manage your subscription, invoices, and payment methods.',
175
+ articleCount: 12,
176
+ icon: 'ph-credit-card',
177
+ url: '#',
178
+ },
179
+ ];
180
+
63
181
  // Mock surveys for development
64
182
  const MOCK_SURVEYS = [
65
183
  {
@@ -481,6 +599,424 @@ export class APIService {
481
599
  }
482
600
  }
483
601
 
602
+ // ==========================================
603
+ // MESSENGER / CHAT ENDPOINTS
604
+ // ==========================================
605
+
606
+ /**
607
+ * Get messenger settings
608
+ * @returns {Promise<Object>} Messenger settings
609
+ */
610
+ async getMessengerSettings() {
611
+ if (!this.isSessionValid()) {
612
+ await this.init();
613
+ }
614
+
615
+ if (this.mock) {
616
+ return {
617
+ status: true,
618
+ data: {
619
+ enabled: true,
620
+ greeting_message: 'Hi there! How can we help you today?',
621
+ team_name: 'Support Team',
622
+ response_time: 'Usually replies within a few minutes',
623
+ },
624
+ };
625
+ }
626
+
627
+ return this._makeRequest('/widget/messenger/settings', {
628
+ method: 'GET',
629
+ headers: { Authorization: `Bearer ${this.sessionToken}` },
630
+ });
631
+ }
632
+
633
+ /**
634
+ * Check if agents are online
635
+ * @returns {Promise<Object>} Agent availability status
636
+ */
637
+ async checkAgentsOnline() {
638
+ if (!this.isSessionValid()) {
639
+ await this.init();
640
+ }
641
+
642
+ if (this.mock) {
643
+ return {
644
+ status: true,
645
+ data: {
646
+ agents_online: true,
647
+ online_count: 2,
648
+ response_time: 'Usually replies within a few minutes',
649
+ },
650
+ };
651
+ }
652
+
653
+ return this._makeRequest('/widget/messenger/agents/online', {
654
+ method: 'GET',
655
+ headers: { Authorization: `Bearer ${this.sessionToken}` },
656
+ });
657
+ }
658
+
659
+ /**
660
+ * Get all conversations for the current contact
661
+ * @param {Object} options - Query options
662
+ * @param {number} options.page - Page number
663
+ * @param {number} options.limit - Items per page
664
+ * @returns {Promise<Object>} Conversations list
665
+ */
666
+ async getConversations(options = {}) {
667
+ if (!this.isSessionValid()) {
668
+ await this.init();
669
+ }
670
+
671
+ if (this.mock) {
672
+ await new Promise((resolve) => setTimeout(resolve, 300));
673
+ return {
674
+ status: true,
675
+ data: MOCK_CONVERSATIONS,
676
+ meta: { total: MOCK_CONVERSATIONS.length, page: 1, limit: 20 },
677
+ };
678
+ }
679
+
680
+ const params = new URLSearchParams();
681
+ if (options.page) params.append('page', options.page);
682
+ if (options.limit) params.append('limit', options.limit);
683
+
684
+ const endpoint = `/widget/messenger/conversations${params.toString() ? '?' + params.toString() : ''}`;
685
+ return this._makeRequest(endpoint, {
686
+ method: 'GET',
687
+ headers: { Authorization: `Bearer ${this.sessionToken}` },
688
+ });
689
+ }
690
+
691
+ /**
692
+ * Get a single conversation with messages
693
+ * @param {string} conversationId - Conversation ID
694
+ * @returns {Promise<Object>} Conversation with messages
695
+ */
696
+ async getConversation(conversationId) {
697
+ if (!this.isSessionValid()) {
698
+ await this.init();
699
+ }
700
+
701
+ if (this.mock) {
702
+ await new Promise((resolve) => setTimeout(resolve, 200));
703
+ const conv = MOCK_CONVERSATIONS.find((c) => c.id === conversationId);
704
+ return {
705
+ status: true,
706
+ data: {
707
+ ...conv,
708
+ messages: MOCK_MESSAGES[conversationId] || [],
709
+ },
710
+ };
711
+ }
712
+
713
+ return this._makeRequest(`/widget/messenger/conversations/${conversationId}`, {
714
+ method: 'GET',
715
+ headers: { Authorization: `Bearer ${this.sessionToken}` },
716
+ });
717
+ }
718
+
719
+ /**
720
+ * Get messages for a conversation
721
+ * @param {string} conversationId - Conversation ID
722
+ * @param {Object} options - Query options
723
+ * @returns {Promise<Object>} Messages list
724
+ */
725
+ async getMessages(conversationId, options = {}) {
726
+ if (!this.isSessionValid()) {
727
+ await this.init();
728
+ }
729
+
730
+ if (this.mock) {
731
+ await new Promise((resolve) => setTimeout(resolve, 200));
732
+ return {
733
+ status: true,
734
+ data: MOCK_MESSAGES[conversationId] || [],
735
+ };
736
+ }
737
+
738
+ const params = new URLSearchParams();
739
+ if (options.page) params.append('page', options.page);
740
+ if (options.limit) params.append('limit', options.limit);
741
+
742
+ const endpoint = `/widget/messenger/conversations/${conversationId}/messages${params.toString() ? '?' + params.toString() : ''}`;
743
+ return this._makeRequest(endpoint, {
744
+ method: 'GET',
745
+ headers: { Authorization: `Bearer ${this.sessionToken}` },
746
+ });
747
+ }
748
+
749
+ /**
750
+ * Start a new conversation
751
+ * @param {Object} data - Conversation data
752
+ * @param {string} data.message - Initial message content
753
+ * @param {string} data.subject - Optional subject
754
+ * @returns {Promise<Object>} Created conversation
755
+ */
756
+ async startConversation(data) {
757
+ if (!this.isSessionValid()) {
758
+ await this.init();
759
+ }
760
+
761
+ if (this.mock) {
762
+ await new Promise((resolve) => setTimeout(resolve, 300));
763
+ const newConv = {
764
+ id: 'conv_' + Date.now(),
765
+ subject: data.subject || 'New conversation',
766
+ status: 'open',
767
+ last_message_at: new Date().toISOString(),
768
+ created_at: new Date().toISOString(),
769
+ messages: [
770
+ {
771
+ id: 'msg_' + Date.now(),
772
+ content: data.message,
773
+ sender_type: 'customer',
774
+ created_at: new Date().toISOString(),
775
+ },
776
+ ],
777
+ };
778
+ MOCK_CONVERSATIONS.unshift(newConv);
779
+ MOCK_MESSAGES[newConv.id] = newConv.messages;
780
+ return { status: true, data: newConv };
781
+ }
782
+
783
+ return this._makeRequest('/widget/messenger/conversations', {
784
+ method: 'POST',
785
+ headers: {
786
+ 'Content-Type': 'application/json',
787
+ Authorization: `Bearer ${this.sessionToken}`,
788
+ },
789
+ body: JSON.stringify({
790
+ message: data.message,
791
+ subject: data.subject || '',
792
+ }),
793
+ });
794
+ }
795
+
796
+ /**
797
+ * Send a message in a conversation
798
+ * @param {string} conversationId - Conversation ID
799
+ * @param {Object} data - Message data
800
+ * @param {string} data.content - Message content
801
+ * @returns {Promise<Object>} Sent message
802
+ */
803
+ async sendMessage(conversationId, data) {
804
+ if (!this.isSessionValid()) {
805
+ await this.init();
806
+ }
807
+
808
+ if (this.mock) {
809
+ await new Promise((resolve) => setTimeout(resolve, 200));
810
+ const newMessage = {
811
+ id: 'msg_' + Date.now(),
812
+ content: data.content,
813
+ sender_type: 'customer',
814
+ created_at: new Date().toISOString(),
815
+ };
816
+ if (!MOCK_MESSAGES[conversationId]) {
817
+ MOCK_MESSAGES[conversationId] = [];
818
+ }
819
+ MOCK_MESSAGES[conversationId].push(newMessage);
820
+ return { status: true, data: newMessage };
821
+ }
822
+
823
+ return this._makeRequest(`/widget/messenger/conversations/${conversationId}/messages`, {
824
+ method: 'POST',
825
+ headers: {
826
+ 'Content-Type': 'application/json',
827
+ Authorization: `Bearer ${this.sessionToken}`,
828
+ },
829
+ body: JSON.stringify({ content: data.content }),
830
+ });
831
+ }
832
+
833
+ /**
834
+ * Send typing indicator
835
+ * @param {string} conversationId - Conversation ID
836
+ * @param {boolean} isTyping - Whether user is typing
837
+ * @returns {Promise<Object>} Response
838
+ */
839
+ async sendTypingIndicator(conversationId, isTyping) {
840
+ if (!this.isSessionValid()) {
841
+ await this.init();
842
+ }
843
+
844
+ if (this.mock) {
845
+ return { status: true };
846
+ }
847
+
848
+ return this._makeRequest(`/widget/messenger/conversations/${conversationId}/typing`, {
849
+ method: 'POST',
850
+ headers: {
851
+ 'Content-Type': 'application/json',
852
+ Authorization: `Bearer ${this.sessionToken}`,
853
+ },
854
+ body: JSON.stringify({ is_typing: isTyping }),
855
+ });
856
+ }
857
+
858
+ /**
859
+ * Mark conversation as read
860
+ * @param {string} conversationId - Conversation ID
861
+ * @returns {Promise<Object>} Response
862
+ */
863
+ async markConversationAsRead(conversationId) {
864
+ if (!this.isSessionValid()) {
865
+ await this.init();
866
+ }
867
+
868
+ if (this.mock) {
869
+ return { status: true };
870
+ }
871
+
872
+ return this._makeRequest(`/widget/messenger/conversations/${conversationId}/read`, {
873
+ method: 'POST',
874
+ headers: { Authorization: `Bearer ${this.sessionToken}` },
875
+ });
876
+ }
877
+
878
+ /**
879
+ * Get unread count
880
+ * @returns {Promise<Object>} Unread count data
881
+ */
882
+ async getUnreadCount() {
883
+ if (!this.isSessionValid()) {
884
+ await this.init();
885
+ }
886
+
887
+ if (this.mock) {
888
+ const count = MOCK_CONVERSATIONS.reduce((sum, c) => sum + (c.unread || 0), 0);
889
+ return {
890
+ status: true,
891
+ data: { unread_count: count, unread_conversations: count > 0 ? 1 : 0 },
892
+ };
893
+ }
894
+
895
+ return this._makeRequest('/widget/messenger/unread', {
896
+ method: 'GET',
897
+ headers: { Authorization: `Bearer ${this.sessionToken}` },
898
+ });
899
+ }
900
+
901
+ /**
902
+ * Submit conversation rating
903
+ * @param {string} conversationId - Conversation ID
904
+ * @param {Object} data - Rating data
905
+ * @param {number} data.rating - Rating (1-5 or thumbs up/down)
906
+ * @param {string} data.comment - Optional comment
907
+ * @returns {Promise<Object>} Response
908
+ */
909
+ async submitRating(conversationId, data) {
910
+ if (!this.isSessionValid()) {
911
+ await this.init();
912
+ }
913
+
914
+ if (this.mock) {
915
+ return { status: true, message: 'Thank you for your feedback!' };
916
+ }
917
+
918
+ return this._makeRequest(`/widget/messenger/conversations/${conversationId}/rate`, {
919
+ method: 'POST',
920
+ headers: {
921
+ 'Content-Type': 'application/json',
922
+ Authorization: `Bearer ${this.sessionToken}`,
923
+ },
924
+ body: JSON.stringify({
925
+ rating: data.rating,
926
+ comment: data.comment || '',
927
+ }),
928
+ });
929
+ }
930
+
931
+ /**
932
+ * Identify contact (for logged-in users)
933
+ * @param {Object} data - Contact data
934
+ * @param {string} data.email - Email address
935
+ * @param {string} data.name - Name
936
+ * @returns {Promise<Object>} Response
937
+ */
938
+ async identifyContact(data) {
939
+ if (!this.isSessionValid()) {
940
+ await this.init();
941
+ }
942
+
943
+ if (this.mock) {
944
+ return { status: true, message: 'Contact identified' };
945
+ }
946
+
947
+ return this._makeRequest('/widget/messenger/identify', {
948
+ method: 'POST',
949
+ headers: {
950
+ 'Content-Type': 'application/json',
951
+ Authorization: `Bearer ${this.sessionToken}`,
952
+ },
953
+ body: JSON.stringify(data),
954
+ });
955
+ }
956
+
957
+ // ==========================================
958
+ // HELP ARTICLES ENDPOINTS
959
+ // ==========================================
960
+
961
+ /**
962
+ * Get help collections
963
+ * @param {Object} options - Query options
964
+ * @returns {Promise<Object>} Collections list
965
+ */
966
+ async getHelpCollections(options = {}) {
967
+ if (!this.isSessionValid()) {
968
+ await this.init();
969
+ }
970
+
971
+ if (this.mock) {
972
+ await new Promise((resolve) => setTimeout(resolve, 200));
973
+ return { status: true, data: MOCK_HELP_COLLECTIONS };
974
+ }
975
+
976
+ const params = new URLSearchParams();
977
+ if (options.limit) params.append('limit', options.limit);
978
+
979
+ const endpoint = `/widget/help/collections${params.toString() ? '?' + params.toString() : ''}`;
980
+ return this._makeRequest(endpoint, {
981
+ method: 'GET',
982
+ headers: { Authorization: `Bearer ${this.sessionToken}` },
983
+ });
984
+ }
985
+
986
+ /**
987
+ * Search help articles
988
+ * @param {string} query - Search query
989
+ * @param {Object} options - Query options
990
+ * @returns {Promise<Object>} Search results
991
+ */
992
+ async searchHelpArticles(query, options = {}) {
993
+ if (!this.isSessionValid()) {
994
+ await this.init();
995
+ }
996
+
997
+ if (this.mock) {
998
+ await new Promise((resolve) => setTimeout(resolve, 200));
999
+ const filtered = MOCK_HELP_COLLECTIONS.filter(
1000
+ (c) =>
1001
+ c.title.toLowerCase().includes(query.toLowerCase()) ||
1002
+ c.description.toLowerCase().includes(query.toLowerCase())
1003
+ );
1004
+ return { status: true, data: filtered };
1005
+ }
1006
+
1007
+ const params = new URLSearchParams({ q: query });
1008
+ if (options.limit) params.append('limit', options.limit);
1009
+
1010
+ return this._makeRequest(`/widget/help/search?${params.toString()}`, {
1011
+ method: 'GET',
1012
+ headers: { Authorization: `Bearer ${this.sessionToken}` },
1013
+ });
1014
+ }
1015
+
1016
+ // ==========================================
1017
+ // CHANGELOG ENDPOINTS
1018
+ // ==========================================
1019
+
484
1020
  /**
485
1021
  * Get published changelogs
486
1022
  * @param {Object} options - Optional query parameters
@@ -345,6 +345,54 @@ export class FeedbackSDK {
345
345
  this.eventBus.emit('sdk:destroyed');
346
346
  }
347
347
 
348
+ /**
349
+ * Check if feedback was recently submitted for this workspace
350
+ * Uses backend tracking (preferred) with localStorage as fallback
351
+ * @param {number} cooldownDays - Days to consider as "recently" (default: 30)
352
+ * @returns {boolean} true if feedback was submitted within the cooldown period
353
+ */
354
+ hasFeedbackBeenSubmitted(cooldownDays = 30) {
355
+ const cooldownMs = cooldownDays * 24 * 60 * 60 * 1000;
356
+ const now = Date.now();
357
+
358
+ // Check backend tracking first (from init response)
359
+ if (this.config.last_feedback_at) {
360
+ try {
361
+ const backendTimestamp = new Date(this.config.last_feedback_at).getTime();
362
+ if ((now - backendTimestamp) < cooldownMs) {
363
+ return true;
364
+ }
365
+ } catch (e) {
366
+ // Invalid date format, continue to localStorage check
367
+ }
368
+ }
369
+
370
+ // Fallback to localStorage
371
+ try {
372
+ const storageKey = `feedback_submitted_${this.config.workspace}`;
373
+ const stored = localStorage.getItem(storageKey);
374
+ if (!stored) return false;
375
+
376
+ const data = JSON.parse(stored);
377
+ return (now - data.submittedAt) < cooldownMs;
378
+ } catch (e) {
379
+ return false;
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Clear the feedback submission tracking (allow showing widgets again)
385
+ */
386
+ clearFeedbackSubmissionTracking() {
387
+ try {
388
+ const storageKey = `feedback_submitted_${this.config.workspace}`;
389
+ localStorage.removeItem(storageKey);
390
+ this.eventBus.emit('feedback:trackingCleared');
391
+ } catch (e) {
392
+ console.warn('Failed to clear feedback tracking:', e);
393
+ }
394
+ }
395
+
348
396
  _detectEnvironment() {
349
397
  if (typeof window === 'undefined') {
350
398
  return 'production';