@product7/product7-js 0.3.4 → 0.3.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/product7-js",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "JavaScript SDK for integrating Product7 feedback widgets into any website",
5
5
  "main": "dist/product7-js.js",
6
6
  "module": "src/index.js",
@@ -21,12 +21,19 @@ export class FeedbackService {
21
21
  };
22
22
  }
23
23
 
24
+ const contact = this.api.getContactIdentity();
25
+
24
26
  const payload = {
25
27
  board:
26
28
  feedbackData.board_id || feedbackData.board || feedbackData.boardName,
27
29
  title: feedbackData.title,
28
30
  content: feedbackData.content,
29
31
  attachments: feedbackData.attachments || [],
32
+ ...(contact?.contactId && {
33
+ contact_id: contact.contactId,
34
+ contact_email: contact.contactEmail,
35
+ contact_name: contact.contactName,
36
+ }),
30
37
  };
31
38
 
32
39
  return this.api._handleAuthRetry(async () => {
@@ -37,6 +37,10 @@ export class MessengerService {
37
37
  agents_online: true,
38
38
  online_count: 2,
39
39
  response_time: 'Usually replies within a few minutes',
40
+ available_agents: [
41
+ { full_name: 'Sarah', picture: '' },
42
+ { full_name: 'Tom', picture: '' },
43
+ ],
40
44
  },
41
45
  };
42
46
  }
@@ -224,10 +228,48 @@ export class MessengerService {
224
228
  });
225
229
  }
226
230
 
231
+ async uploadFile(base64Data, filename) {
232
+ await this.api._ensureSession();
233
+
234
+ if (this.api.mock) {
235
+ await delay(300);
236
+ return { status: true, url: `https://mock-cdn.example.com/${filename}` };
237
+ }
238
+
239
+ return this.api._makeRequest('/widget/messenger/upload', {
240
+ method: 'POST',
241
+ headers: {
242
+ 'Content-Type': 'application/json',
243
+ Authorization: `Bearer ${this.api.sessionToken}`,
244
+ },
245
+ body: JSON.stringify({ file: base64Data, filename }),
246
+ });
247
+ }
248
+
227
249
  async identifyContact(data) {
228
250
  return this.api.identify(data);
229
251
  }
230
252
 
253
+ async submitRating(conversationId, data) {
254
+ await this.api._ensureSession();
255
+
256
+ if (this.api.mock) {
257
+ return { status: true, data: { rated: true } };
258
+ }
259
+
260
+ return this.api._makeRequest(
261
+ `/widget/messenger/conversations/${conversationId}/rating`,
262
+ {
263
+ method: 'POST',
264
+ headers: {
265
+ 'Content-Type': 'application/json',
266
+ Authorization: `Bearer ${this.api.sessionToken}`,
267
+ },
268
+ body: JSON.stringify(data),
269
+ }
270
+ );
271
+ }
272
+
231
273
  async sendTypingIndicator(conversationId, isTyping) {
232
274
  await this.api._ensureSession();
233
275
 
@@ -78,6 +78,7 @@ export class SurveyService {
78
78
  }
79
79
 
80
80
  const respondent = this._getRespondentContext(responseData);
81
+ const contact = this.api.getContactIdentity?.() || null;
81
82
 
82
83
  const payload = {
83
84
  rating: responseData.rating,
@@ -87,6 +88,11 @@ export class SurveyService {
87
88
  respondent_id: respondent.respondent_id,
88
89
  }),
89
90
  ...(respondent.email && { email: respondent.email }),
91
+ ...(contact?.contactId && {
92
+ contact_id: contact.contactId,
93
+ contact_email: contact.contactEmail,
94
+ contact_name: contact.contactName,
95
+ }),
90
96
  };
91
97
 
92
98
  return this.api._handleAuthRetry(async () => {
@@ -37,29 +37,7 @@ export class APIService extends BaseAPIService {
37
37
  }
38
38
 
39
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
- });
40
+ return this.messenger.checkAgentsOnline();
63
41
  }
64
42
 
65
43
  async getConversations(options) {
@@ -75,137 +53,15 @@ export class APIService extends BaseAPIService {
75
53
  }
76
54
 
77
55
  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
- });
56
+ return this.messenger.startConversation(data);
144
57
  }
145
58
 
146
59
  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
- );
60
+ return this.messenger.sendMessage(conversationId, data);
183
61
  }
184
62
 
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
63
  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
- });
64
+ return this.messenger.uploadFile(base64Data, filename);
209
65
  }
210
66
 
211
67
  async sendTypingIndicator(conversationId, isTyping) {
@@ -229,7 +85,7 @@ export class APIService extends BaseAPIService {
229
85
 
230
86
  if (this.mock) {
231
87
  await new Promise((r) => setTimeout(r, 300));
232
- return {
88
+ const mockResponse = {
233
89
  status: true,
234
90
  data: {
235
91
  contact_id: 'mock_contact_' + Date.now(),
@@ -238,9 +94,11 @@ export class APIService extends BaseAPIService {
238
94
  is_new: true,
239
95
  },
240
96
  };
97
+ this._storeContactIdentity(mockResponse.data, metadata);
98
+ return mockResponse;
241
99
  }
242
100
 
243
- return this._makeRequest('/widget/messenger/identify', {
101
+ const response = await this._makeRequest('/widget/messenger/identify', {
244
102
  method: 'POST',
245
103
  headers: {
246
104
  'Content-Type': 'application/json',
@@ -256,82 +114,71 @@ export class APIService extends BaseAPIService {
256
114
  metadata: metadata.custom_fields || {},
257
115
  }),
258
116
  });
259
- }
260
-
261
- async identifyContact(data) {
262
- return this.identify(data);
263
- }
264
-
265
- async getHelpCollections(options) {
266
- return this.help.getHelpCollections(options);
267
- }
268
117
 
269
- async searchHelpArticles(query, options) {
270
- return this.help.searchHelpArticles(query, options);
271
- }
118
+ if (response?.status && response?.data) {
119
+ this._storeContactIdentity(response.data, metadata);
120
+ }
272
121
 
273
- async getChangelogs(options) {
274
- return this.changelog.getChangelogs(options);
122
+ return response;
275
123
  }
276
124
 
277
- _loadStoredSession() {
278
- if (typeof localStorage === 'undefined') return false;
125
+ _storeContactIdentity(data, metadata = {}) {
126
+ this.contactId = data.contact_id || null;
127
+ this.contactEmail = data.email || metadata.email || null;
128
+ this.contactName = data.name || metadata.name || null;
279
129
 
280
130
  try {
281
- const stored = localStorage.getItem('product7_session');
282
- if (!stored) return false;
283
-
284
- const sessionData = JSON.parse(stored);
285
-
286
- // Invalidate mock tokens when not in mock mode (and vice versa)
287
- const isMockToken =
288
- sessionData.token && sessionData.token.startsWith('mock_');
289
- if (isMockToken !== this.mock) {
290
- localStorage.removeItem('product7_session');
291
- return false;
292
- }
293
-
294
- this.sessionToken = sessionData.token;
295
- this.sessionExpiry = new Date(sessionData.expiry);
296
-
297
- return this.isSessionValid();
298
- } catch (error) {
299
- return false;
131
+ localStorage.setItem(
132
+ 'product7_contact',
133
+ JSON.stringify({
134
+ contactId: this.contactId,
135
+ contactEmail: this.contactEmail,
136
+ contactName: this.contactName,
137
+ })
138
+ );
139
+ } catch (e) {
140
+ /* silent */
300
141
  }
301
142
  }
302
143
 
303
- async _makeRequest(endpoint, options = {}) {
304
- const url = `${this.baseURL}${endpoint}`;
144
+ getContactIdentity() {
145
+ if (this.contactId) {
146
+ return {
147
+ contactId: this.contactId,
148
+ contactEmail: this.contactEmail,
149
+ contactName: this.contactName,
150
+ };
151
+ }
305
152
 
306
153
  try {
307
- const response = await fetch(url, options);
154
+ const stored = localStorage.getItem('product7_contact');
155
+ if (stored) {
156
+ const parsed = JSON.parse(stored);
157
+ this.contactId = parsed.contactId;
158
+ this.contactEmail = parsed.contactEmail;
159
+ this.contactName = parsed.contactName;
160
+ return parsed;
161
+ }
162
+ } catch (e) {
163
+ /* silent */
164
+ }
308
165
 
309
- if (!response.ok) {
310
- let errorMessage = `HTTP ${response.status}`;
311
- let responseData = null;
166
+ return null;
167
+ }
312
168
 
313
- try {
314
- responseData = await response.json();
315
- errorMessage =
316
- responseData.message || responseData.error || errorMessage;
317
- } catch (e) {
318
- errorMessage = (await response.text()) || errorMessage;
319
- }
169
+ async identifyContact(data) {
170
+ return this.identify(data);
171
+ }
320
172
 
321
- throw new APIError(response.status, errorMessage, responseData);
322
- }
173
+ async getHelpCollections(options) {
174
+ return this.help.getHelpCollections(options);
175
+ }
323
176
 
324
- const contentType = response.headers.get('content-type');
325
- if (contentType && contentType.includes('application/json')) {
326
- return await response.json();
327
- }
177
+ async searchHelpArticles(query, options) {
178
+ return this.help.searchHelpArticles(query, options);
179
+ }
328
180
 
329
- return await response.text();
330
- } catch (error) {
331
- if (error instanceof APIError) {
332
- throw error;
333
- }
334
- throw new APIError(0, error.message, null);
335
- }
181
+ async getChangelogs(options) {
182
+ return this.changelog.getChangelogs(options);
336
183
  }
337
184
  }
@@ -246,8 +246,12 @@ export class BaseAPIService {
246
246
  this.sessionToken = null;
247
247
  this.sessionExpiry = null;
248
248
  this.identitySyncedToken = null;
249
+ this.contactId = null;
250
+ this.contactEmail = null;
251
+ this.contactName = null;
249
252
  this._removeData('product7_session');
250
253
  this._removeData('product7_metadata');
254
+ this._removeData('product7_contact');
251
255
  }
252
256
 
253
257
  _storeSession() {
@@ -271,6 +275,15 @@ export class BaseAPIService {
271
275
  if (!stored) return false;
272
276
 
273
277
  const sessionData = JSON.parse(stored);
278
+
279
+ // Invalidate mock tokens when not in mock mode (and vice versa)
280
+ const isMockToken =
281
+ sessionData.token && sessionData.token.startsWith('mock_');
282
+ if (isMockToken !== this.mock) {
283
+ localStorage.removeItem('product7_session');
284
+ return false;
285
+ }
286
+
274
287
  if (
275
288
  this.workspace &&
276
289
  sessionData.workspace &&
@@ -279,6 +292,7 @@ export class BaseAPIService {
279
292
  localStorage.removeItem('product7_session');
280
293
  return false;
281
294
  }
295
+
282
296
  this.sessionToken = sessionData.token;
283
297
  this.sessionExpiry = new Date(sessionData.expiry);
284
298
 
@@ -862,7 +862,7 @@ export class Product7 {
862
862
  metadata: null,
863
863
  position: 'right',
864
864
  theme: 'light',
865
- boardName: 'general',
865
+ boardName: 'feature-requests',
866
866
  autoShow: true,
867
867
  debug: false,
868
868
  mock: false,
@@ -58,6 +58,7 @@ export class MessengerWidget extends BaseWidget {
58
58
  initialView: options.initialView || 'home',
59
59
  previewData: options.previewData || null,
60
60
  featuredContent: options.featuredContent || null,
61
+ feedbackBoardName: options.feedbackBoardName || null,
61
62
  feedbackUrl: options.feedbackUrl || null,
62
63
  changelogUrl: options.changelogUrl || null,
63
64
  helpUrl: options.helpUrl || null,
@@ -109,7 +110,8 @@ export class MessengerWidget extends BaseWidget {
109
110
  const widget = this.sdk.createWidget('button', {
110
111
  trigger: false,
111
112
  displayMode: 'modal',
112
- boardName: this.sdk.config.boardName,
113
+ boardName:
114
+ this.messengerOptions.feedbackBoardName || this.sdk.config.boardName,
113
115
  primaryColor: this.messengerOptions.primaryColor,
114
116
  theme: this.messengerOptions.theme,
115
117
  });