@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
@@ -0,0 +1,303 @@
1
+ import { ChangelogService } from '../api/services/ChangelogService.js';
2
+ import { FeedbackService } from '../api/services/FeedbackService.js';
3
+ import { HelpService } from '../api/services/HelpService.js';
4
+ import { MessengerService } from '../api/services/MessengerService.js';
5
+ import { SurveyService } from '../api/services/SurveyService.js';
6
+ import { BaseAPIService } from './BaseAPIService.js';
7
+
8
+ export class APIService extends BaseAPIService {
9
+ constructor(config = {}) {
10
+ super(config);
11
+
12
+ this.feedback = new FeedbackService(this);
13
+ this.survey = new SurveyService(this);
14
+ this.messenger = new MessengerService(this);
15
+ this.help = new HelpService(this);
16
+ this.changelog = new ChangelogService(this);
17
+ }
18
+
19
+ async submitFeedback(data) {
20
+ return this.feedback.submitFeedback(data);
21
+ }
22
+
23
+ async getActiveSurveys(context) {
24
+ return this.survey.getActiveSurveys(context);
25
+ }
26
+
27
+ async submitSurveyResponse(surveyId, responseData) {
28
+ return this.survey.submitSurveyResponse(surveyId, responseData);
29
+ }
30
+
31
+ async dismissSurvey(surveyId) {
32
+ return this.survey.dismissSurvey(surveyId);
33
+ }
34
+
35
+ async getMessengerSettings() {
36
+ return this.messenger.getMessengerSettings();
37
+ }
38
+
39
+ async checkAgentsOnline() {
40
+ if (!this.isSessionValid()) {
41
+ await this.init();
42
+ }
43
+
44
+ if (this.mock) {
45
+ return {
46
+ status: true,
47
+ data: {
48
+ agents_online: true,
49
+ online_count: 2,
50
+ response_time: 'Usually replies within a few minutes',
51
+ available_agents: [
52
+ { full_name: 'Sarah', picture: '' },
53
+ { full_name: 'Tom', picture: '' },
54
+ ],
55
+ },
56
+ };
57
+ }
58
+
59
+ return this._makeRequest('/widget/messenger/agents/online', {
60
+ method: 'GET',
61
+ headers: { Authorization: `Bearer ${this.sessionToken}` },
62
+ });
63
+ }
64
+
65
+ async getConversations(options) {
66
+ return this.messenger.getConversations(options);
67
+ }
68
+
69
+ async getConversation(conversationId) {
70
+ return this.messenger.getConversation(conversationId);
71
+ }
72
+
73
+ async getMessages(conversationId, options) {
74
+ return this.messenger.getMessages(conversationId, options);
75
+ }
76
+
77
+ async startConversation(data) {
78
+ if (!this.isSessionValid()) {
79
+ console.log(
80
+ '[APIService] startConversation: session invalid, calling init...'
81
+ );
82
+ try {
83
+ await this.init();
84
+ console.log(
85
+ '[APIService] startConversation: init result, token:',
86
+ this.sessionToken ? 'set' : 'null'
87
+ );
88
+ } catch (initError) {
89
+ console.error(
90
+ '[APIService] startConversation: init failed:',
91
+ initError.message
92
+ );
93
+ throw initError;
94
+ }
95
+ }
96
+
97
+ if (!this.sessionToken) {
98
+ console.error(
99
+ '[APIService] startConversation: no session token after init'
100
+ );
101
+ throw new APIError(401, 'No valid session token available');
102
+ }
103
+
104
+ console.log(
105
+ '[APIService] startConversation: sending to',
106
+ `${this.baseURL}/widget/messenger/conversations`,
107
+ 'mock:',
108
+ this.mock
109
+ );
110
+
111
+ if (this.mock) {
112
+ await new Promise((resolve) => setTimeout(resolve, 300));
113
+ const newConv = {
114
+ id: 'conv_' + Date.now(),
115
+ subject: data.subject || 'New conversation',
116
+ status: 'open',
117
+ last_message_at: new Date().toISOString(),
118
+ created_at: new Date().toISOString(),
119
+ messages: [
120
+ {
121
+ id: 'msg_' + Date.now(),
122
+ content: data.message,
123
+ sender_type: 'customer',
124
+ created_at: new Date().toISOString(),
125
+ },
126
+ ],
127
+ };
128
+ MOCK_CONVERSATIONS.unshift(newConv);
129
+ MOCK_MESSAGES[newConv.id] = newConv.messages;
130
+ return { status: true, data: newConv };
131
+ }
132
+
133
+ return this._makeRequest('/widget/messenger/conversations', {
134
+ method: 'POST',
135
+ headers: {
136
+ 'Content-Type': 'application/json',
137
+ Authorization: `Bearer ${this.sessionToken}`,
138
+ },
139
+ body: JSON.stringify({
140
+ message: data.message,
141
+ subject: data.subject || '',
142
+ }),
143
+ });
144
+ }
145
+
146
+ async sendMessage(conversationId, data) {
147
+ if (!this.isSessionValid()) {
148
+ await this.init();
149
+ }
150
+
151
+ if (this.mock) {
152
+ await new Promise((resolve) => setTimeout(resolve, 200));
153
+ const newMessage = {
154
+ id: 'msg_' + Date.now(),
155
+ content: data.content,
156
+ attachments: data.attachments || [],
157
+ sender_type: 'customer',
158
+ created_at: new Date().toISOString(),
159
+ };
160
+ if (!MOCK_MESSAGES[conversationId]) {
161
+ MOCK_MESSAGES[conversationId] = [];
162
+ }
163
+ MOCK_MESSAGES[conversationId].push(newMessage);
164
+ return { status: true, data: newMessage };
165
+ }
166
+
167
+ const payload = { content: data.content };
168
+ if (data.attachments && data.attachments.length > 0) {
169
+ payload.attachments = data.attachments;
170
+ }
171
+
172
+ return this._makeRequest(
173
+ `/widget/messenger/conversations/${conversationId}/messages`,
174
+ {
175
+ method: 'POST',
176
+ headers: {
177
+ 'Content-Type': 'application/json',
178
+ Authorization: `Bearer ${this.sessionToken}`,
179
+ },
180
+ body: JSON.stringify(payload),
181
+ }
182
+ );
183
+ }
184
+
185
+ /**
186
+ * Upload a file to CDN via widget endpoint
187
+ * @param {string} base64Data - Base64 encoded file data (with or without data URI prefix)
188
+ * @param {string} filename - Original filename
189
+ * @returns {Promise<Object>} Response with url
190
+ */
191
+ async uploadFile(base64Data, filename) {
192
+ if (!this.isSessionValid()) {
193
+ await this.init();
194
+ }
195
+
196
+ if (this.mock) {
197
+ await new Promise((resolve) => setTimeout(resolve, 300));
198
+ return { status: true, url: `https://mock-cdn.example.com/${filename}` };
199
+ }
200
+
201
+ return this._makeRequest('/widget/messenger/upload', {
202
+ method: 'POST',
203
+ headers: {
204
+ 'Content-Type': 'application/json',
205
+ Authorization: `Bearer ${this.sessionToken}`,
206
+ },
207
+ body: JSON.stringify({ file: base64Data, filename }),
208
+ });
209
+ }
210
+
211
+ async sendTypingIndicator(conversationId, isTyping) {
212
+ return this.messenger.sendTypingIndicator(conversationId, isTyping);
213
+ }
214
+
215
+ async markConversationAsRead(conversationId) {
216
+ return this.messenger.markConversationAsRead(conversationId);
217
+ }
218
+
219
+ async getUnreadCount() {
220
+ return this.messenger.getUnreadCount();
221
+ }
222
+
223
+ async submitRating(conversationId, data) {
224
+ return this.messenger.submitRating(conversationId, data);
225
+ }
226
+
227
+ async identifyContact(data) {
228
+ return this.messenger.identifyContact(data);
229
+ }
230
+
231
+ async getHelpCollections(options) {
232
+ return this.help.getHelpCollections(options);
233
+ }
234
+
235
+ async searchHelpArticles(query, options) {
236
+ return this.help.searchHelpArticles(query, options);
237
+ }
238
+
239
+ async getChangelogs(options) {
240
+ return this.changelog.getChangelogs(options);
241
+ }
242
+
243
+ _loadStoredSession() {
244
+ if (typeof localStorage === 'undefined') return false;
245
+
246
+ try {
247
+ const stored = localStorage.getItem('product7_session');
248
+ if (!stored) return false;
249
+
250
+ const sessionData = JSON.parse(stored);
251
+
252
+ // Invalidate mock tokens when not in mock mode (and vice versa)
253
+ const isMockToken =
254
+ sessionData.token && sessionData.token.startsWith('mock_');
255
+ if (isMockToken !== this.mock) {
256
+ localStorage.removeItem('product7_session');
257
+ return false;
258
+ }
259
+
260
+ this.sessionToken = sessionData.token;
261
+ this.sessionExpiry = new Date(sessionData.expiry);
262
+
263
+ return this.isSessionValid();
264
+ } catch (error) {
265
+ return false;
266
+ }
267
+ }
268
+
269
+ async _makeRequest(endpoint, options = {}) {
270
+ const url = `${this.baseURL}${endpoint}`;
271
+
272
+ try {
273
+ const response = await fetch(url, options);
274
+
275
+ if (!response.ok) {
276
+ let errorMessage = `HTTP ${response.status}`;
277
+ let responseData = null;
278
+
279
+ try {
280
+ responseData = await response.json();
281
+ errorMessage =
282
+ responseData.message || responseData.error || errorMessage;
283
+ } catch (e) {
284
+ errorMessage = (await response.text()) || errorMessage;
285
+ }
286
+
287
+ throw new APIError(response.status, errorMessage, responseData);
288
+ }
289
+
290
+ const contentType = response.headers.get('content-type');
291
+ if (contentType && contentType.includes('application/json')) {
292
+ return await response.json();
293
+ }
294
+
295
+ return await response.text();
296
+ } catch (error) {
297
+ if (error instanceof APIError) {
298
+ throw error;
299
+ }
300
+ throw new APIError(0, error.message, null);
301
+ }
302
+ }
303
+ }
@@ -0,0 +1,298 @@
1
+ import { APIError } from '../utils/errors.js';
2
+
3
+ export class BaseAPIService {
4
+ constructor(config = {}) {
5
+ this.workspace = config.workspace;
6
+ this.sessionToken = null;
7
+ this.sessionExpiry = null;
8
+ this.metadata = config.metadata || null;
9
+ this.mock = config.mock || false;
10
+ this.env = config.env || 'production';
11
+ this.baseURL = this._getBaseURL(config);
12
+
13
+ this._loadStoredSession();
14
+ }
15
+
16
+ _getBaseURL(config) {
17
+ if (config.apiUrl) return config.apiUrl;
18
+
19
+ const ENV_URLS = {
20
+ production: {
21
+ base: 'https://api.product7.io/api/v1',
22
+ withWorkspace: (ws) => `https://${ws}.api.product7.io/api/v1`,
23
+ },
24
+ staging: {
25
+ base: 'https://staging.api.product7.io/api/v1',
26
+ withWorkspace: (ws) => `https://${ws}.staging.api.product7.io/api/v1`,
27
+ },
28
+ localstack: {
29
+ base: 'http://localhost:1323/api/v1',
30
+ withWorkspace: (ws) => `http://${ws}.localhost:1323/api/v1`,
31
+ },
32
+ };
33
+
34
+ let env = this.env;
35
+ if (!env || env === 'production') {
36
+ const hostname =
37
+ (typeof window !== 'undefined' && window.location?.hostname) || '';
38
+ const isLocal =
39
+ hostname === 'localhost' ||
40
+ hostname === '127.0.0.1' ||
41
+ /^192\.168\./.test(hostname) ||
42
+ /^10\./.test(hostname) ||
43
+ /^172\.(1[6-9]|2\d|3[01])\./.test(hostname);
44
+ if (hostname.includes('staging') || isLocal) {
45
+ env = 'staging';
46
+ }
47
+ }
48
+
49
+ const envConfig = ENV_URLS[env] || ENV_URLS.production;
50
+ return this.workspace
51
+ ? envConfig.withWorkspace(this.workspace)
52
+ : envConfig.base;
53
+ }
54
+
55
+ async init(metadata = null) {
56
+ if (metadata) {
57
+ this.metadata = metadata;
58
+ }
59
+
60
+ if (this.isSessionValid()) {
61
+ return { sessionToken: this.sessionToken };
62
+ }
63
+
64
+ if (!this.workspace || !this.metadata) {
65
+ throw new APIError(
66
+ 400,
67
+ `Missing ${!this.workspace ? 'workspace' : 'user context'} for initialization`
68
+ );
69
+ }
70
+
71
+ if (this.mock) {
72
+ return this._initMockSession();
73
+ }
74
+
75
+ return this._initRealSession();
76
+ }
77
+
78
+ async _initMockSession() {
79
+ this.sessionToken = 'mock_session_' + Date.now();
80
+ this.sessionExpiry = new Date(Date.now() + 3600 * 1000);
81
+ this._storeSession();
82
+ return {
83
+ sessionToken: this.sessionToken,
84
+ config: {
85
+ primaryColor: '#21244A',
86
+ backgroundColor: '#ffffff',
87
+ textColor: '#1F2937',
88
+ boardName: 'feature-requests',
89
+ size: 'medium',
90
+ displayMode: 'modal',
91
+ },
92
+ expiresIn: 3600,
93
+ };
94
+ }
95
+
96
+ async _initRealSession() {
97
+ const payload = {
98
+ workspace: this.workspace,
99
+ user: this.metadata,
100
+ };
101
+
102
+ try {
103
+ const response = await this._makeRequest('/widget/init', {
104
+ method: 'POST',
105
+ body: JSON.stringify(payload),
106
+ headers: { 'Content-Type': 'application/json' },
107
+ });
108
+ const initData = this._extractInitResponseData(response);
109
+
110
+ this.sessionToken = initData.sessionToken;
111
+ this.sessionExpiry = new Date(Date.now() + initData.expiresIn * 1000);
112
+ this._storeSession();
113
+
114
+ return {
115
+ sessionToken: this.sessionToken,
116
+ config: initData.config,
117
+ expiresIn: initData.expiresIn,
118
+ status: initData.status,
119
+ message: initData.message,
120
+ configVersion: initData.configVersion,
121
+ };
122
+ } catch (error) {
123
+ throw new APIError(
124
+ error.status || 500,
125
+ `Failed to initialize widget: ${error.message}`,
126
+ error.response
127
+ );
128
+ }
129
+ }
130
+
131
+ _extractInitResponseData(response) {
132
+ const payload =
133
+ response && typeof response.data === 'object'
134
+ ? response.data
135
+ : response || {};
136
+
137
+ const sessionToken = payload.session_token || payload.sessionToken;
138
+ const expiresIn = Number(payload.expires_in ?? payload.expiresIn);
139
+
140
+ if (!sessionToken) {
141
+ throw new APIError(500, 'Invalid init response: missing session_token');
142
+ }
143
+
144
+ if (!Number.isFinite(expiresIn) || expiresIn <= 0) {
145
+ throw new APIError(500, 'Invalid init response: missing expires_in');
146
+ }
147
+
148
+ return {
149
+ sessionToken,
150
+ expiresIn,
151
+ config:
152
+ payload.config && typeof payload.config === 'object'
153
+ ? payload.config
154
+ : {},
155
+ configVersion: payload.config_version ?? payload.configVersion ?? null,
156
+ status: response?.status ?? payload?.status ?? true,
157
+ message: response?.message ?? payload?.message ?? null,
158
+ };
159
+ }
160
+
161
+ async _ensureSession() {
162
+ if (!this.isSessionValid()) {
163
+ await this.init();
164
+ }
165
+ if (!this.sessionToken) {
166
+ throw new APIError(401, 'No valid session token available');
167
+ }
168
+ }
169
+
170
+ async _handleAuthRetry(method, ...args) {
171
+ try {
172
+ return await method.apply(this, args);
173
+ } catch (error) {
174
+ if (error.status === 401) {
175
+ this.sessionToken = null;
176
+ this.sessionExpiry = null;
177
+ await this.init();
178
+ return await method.apply(this, args);
179
+ }
180
+ throw error;
181
+ }
182
+ }
183
+
184
+ isSessionValid() {
185
+ return (
186
+ this.sessionToken && this.sessionExpiry && new Date() < this.sessionExpiry
187
+ );
188
+ }
189
+
190
+ setMetadata(metadata) {
191
+ this.metadata = metadata;
192
+ this._storeData('product7_metadata', metadata);
193
+ }
194
+
195
+ getMetadata() {
196
+ return this.metadata;
197
+ }
198
+
199
+ clearSession() {
200
+ this.sessionToken = null;
201
+ this.sessionExpiry = null;
202
+ this._removeData('product7_session');
203
+ this._removeData('product7_metadata');
204
+ }
205
+
206
+ _storeSession() {
207
+ if (typeof localStorage === 'undefined') return;
208
+ try {
209
+ const sessionData = {
210
+ token: this.sessionToken,
211
+ expiry: this.sessionExpiry.toISOString(),
212
+ workspace: this.workspace,
213
+ };
214
+ this._storeData('product7_session', sessionData);
215
+ } catch (error) {
216
+ // Silent fail
217
+ }
218
+ }
219
+
220
+ _loadStoredSession() {
221
+ if (typeof localStorage === 'undefined') return false;
222
+ try {
223
+ const stored = localStorage.getItem('product7_session');
224
+ if (!stored) return false;
225
+
226
+ const sessionData = JSON.parse(stored);
227
+ this.sessionToken = sessionData.token;
228
+ this.sessionExpiry = new Date(sessionData.expiry);
229
+
230
+ return this.isSessionValid();
231
+ } catch (error) {
232
+ return false;
233
+ }
234
+ }
235
+
236
+ _storeData(key, value) {
237
+ if (typeof localStorage !== 'undefined') {
238
+ localStorage.setItem(key, JSON.stringify(value));
239
+ }
240
+ }
241
+
242
+ _removeData(key) {
243
+ if (typeof localStorage !== 'undefined') {
244
+ localStorage.removeItem(key);
245
+ }
246
+ }
247
+
248
+ async _makeRequest(endpoint, options = {}) {
249
+ const url = `${this.baseURL}${endpoint}`;
250
+
251
+ try {
252
+ const response = await fetch(url, options);
253
+
254
+ if (!response.ok) {
255
+ let errorMessage = `HTTP ${response.status}`;
256
+ let responseData = null;
257
+
258
+ try {
259
+ responseData = await response.json();
260
+ errorMessage =
261
+ responseData.message || responseData.error || errorMessage;
262
+ } catch (e) {
263
+ errorMessage = (await response.text()) || errorMessage;
264
+ }
265
+
266
+ throw new APIError(response.status, errorMessage, responseData);
267
+ }
268
+
269
+ const contentType = response.headers.get('content-type');
270
+ if (contentType && contentType.includes('application/json')) {
271
+ return await response.json();
272
+ }
273
+
274
+ return await response.text();
275
+ } catch (error) {
276
+ if (error instanceof APIError) throw error;
277
+ throw new APIError(0, error.message, null);
278
+ }
279
+ }
280
+
281
+ _buildQueryParams(params) {
282
+ const queryParams = new URLSearchParams();
283
+ Object.entries(params).forEach(([key, value]) => {
284
+ if (value !== undefined && value !== null) {
285
+ queryParams.append(
286
+ key,
287
+ typeof value === 'object' ? JSON.stringify(value) : value
288
+ );
289
+ }
290
+ });
291
+ return queryParams.toString();
292
+ }
293
+
294
+ _getEndpointWithParams(endpoint, params) {
295
+ const queryString = this._buildQueryParams(params);
296
+ return `${endpoint}${queryString ? '?' + queryString : ''}`;
297
+ }
298
+ }
@@ -0,0 +1,54 @@
1
+ export class EventBus {
2
+ constructor() {
3
+ this.events = new Map();
4
+ }
5
+
6
+ on(event, callback) {
7
+ if (!this.events.has(event)) {
8
+ this.events.set(event, []);
9
+ }
10
+ this.events.get(event).push(callback);
11
+
12
+ return () => this.off(event, callback);
13
+ }
14
+
15
+ off(event, callback) {
16
+ const callbacks = this.events.get(event);
17
+ if (callbacks) {
18
+ const index = callbacks.indexOf(callback);
19
+ if (index > -1) {
20
+ callbacks.splice(index, 1);
21
+ }
22
+ }
23
+ }
24
+
25
+ emit(event, data) {
26
+ const callbacks = this.events.get(event);
27
+ if (callbacks) {
28
+ callbacks.forEach((callback) => {
29
+ try {
30
+ callback(data);
31
+ } catch (error) {
32
+ console.error('[Product7] Event callback error:', error);
33
+ }
34
+ });
35
+ }
36
+ }
37
+
38
+ once(event, callback) {
39
+ const unsubscribe = this.on(event, (data) => {
40
+ callback(data);
41
+ unsubscribe();
42
+ });
43
+ return unsubscribe;
44
+ }
45
+
46
+ clear() {
47
+ this.events.clear();
48
+ }
49
+
50
+ getListenerCount(event) {
51
+ const callbacks = this.events.get(event);
52
+ return callbacks ? callbacks.length : 0;
53
+ }
54
+ }