@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.
- package/README.md +1025 -0
- package/dist/README.md +1025 -0
- package/dist/product7-js.js +14658 -0
- package/dist/product7-js.js.map +1 -0
- package/dist/product7-js.min.js +2 -0
- package/dist/product7-js.min.js.map +1 -0
- package/package.json +114 -0
- package/src/api/mock-data/index.js +360 -0
- package/src/api/services/ChangelogService.js +28 -0
- package/src/api/services/FeedbackService.js +44 -0
- package/src/api/services/HelpService.js +50 -0
- package/src/api/services/MessengerService.js +279 -0
- package/src/api/services/SurveyService.js +127 -0
- package/src/api/utils/helpers.js +30 -0
- package/src/core/APIService.js +303 -0
- package/src/core/BaseAPIService.js +298 -0
- package/src/core/EventBus.js +54 -0
- package/src/core/Product7.js +812 -0
- package/src/core/WebSocketService.js +275 -0
- package/src/docs/api.md +226 -0
- package/src/docs/example.md +461 -0
- package/src/docs/framework-integrations.md +714 -0
- package/src/docs/installation.md +281 -0
- package/src/index.js +894 -0
- package/src/styles/base.js +50 -0
- package/src/styles/changelog.js +665 -0
- package/src/styles/components.js +553 -0
- package/src/styles/design-tokens.js +124 -0
- package/src/styles/feedback.js +325 -0
- package/src/styles/messenger-components.js +632 -0
- package/src/styles/messenger-core.js +233 -0
- package/src/styles/messenger-features.js +169 -0
- package/src/styles/messenger-views.js +877 -0
- package/src/styles/messenger.js +17 -0
- package/src/styles/messengerCustomStyles.js +114 -0
- package/src/styles/styles.js +26 -0
- package/src/styles/survey.js +894 -0
- package/src/utils/errors.js +142 -0
- package/src/utils/helpers.js +219 -0
- package/src/widgets/BaseWidget.js +548 -0
- package/src/widgets/ButtonWidget.js +104 -0
- package/src/widgets/ChangelogWidget.js +615 -0
- package/src/widgets/InlineWidget.js +148 -0
- package/src/widgets/MessengerWidget.js +979 -0
- package/src/widgets/SurveyWidget.js +1325 -0
- package/src/widgets/TabWidget.js +45 -0
- package/src/widgets/WidgetFactory.js +70 -0
- package/src/widgets/messenger/MessengerState.js +323 -0
- package/src/widgets/messenger/components/MessengerLauncher.js +124 -0
- package/src/widgets/messenger/components/MessengerPanel.js +111 -0
- package/src/widgets/messenger/components/NavigationTabs.js +130 -0
- package/src/widgets/messenger/views/ChangelogView.js +167 -0
- package/src/widgets/messenger/views/ChatView.js +592 -0
- package/src/widgets/messenger/views/ConversationsView.js +244 -0
- package/src/widgets/messenger/views/HelpView.js +239 -0
- package/src/widgets/messenger/views/HomeView.js +300 -0
- package/src/widgets/messenger/views/PreChatFormView.js +109 -0
- 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
|
+
}
|