@product7/product7-js 0.3.2 → 0.3.4

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Package Installation
4
4
 
5
- ### NPM
5
+ ### npm
6
6
 
7
7
  ```bash
8
8
  npm install @product7/product7-js
@@ -17,34 +17,50 @@ yarn add @product7/product7-js
17
17
  ### CDN
18
18
 
19
19
  ```html
20
- <script src="https://cdn.product7.io/product7-js/latest/product7-js.js"></script>
20
+ <script src="https://cdn.jsdelivr.net/npm/@product7/product7-js@latest/dist/product7-js.min.js"></script>
21
21
  ```
22
22
 
23
23
  ---
24
24
 
25
25
  ## Quick Setup
26
26
 
27
- ### 1. Import and Initialize
27
+ ### 1. Create the SDK
28
28
 
29
29
  ```javascript
30
- import Product7 from '@product7/product7-js';
30
+ import { Product7 } from '@product7/product7-js';
31
31
 
32
32
  const sdk = new Product7({
33
33
  workspace: 'your-workspace',
34
- metadata: {
35
- user_id: 'user_123',
36
- email: 'user@example.com',
37
- name: 'John Doe',
38
- },
34
+ boardName: 'feature-requests',
39
35
  });
36
+ ```
40
37
 
38
+ ### 2. Initialize the session
39
+
40
+ ```javascript
41
41
  await sdk.init();
42
42
  ```
43
43
 
44
- ### 2. Create Widget
44
+ ### 3. Identify the current user
45
45
 
46
46
  ```javascript
47
- const widget = sdk.createWidget('button');
47
+ await sdk.identify({
48
+ user_id: 'user_123',
49
+ email: 'user@example.com',
50
+ name: 'John Doe',
51
+ custom_fields: {
52
+ plan: 'pro',
53
+ role: 'admin',
54
+ },
55
+ });
56
+ ```
57
+
58
+ ### 4. Create and mount a widget
59
+
60
+ ```javascript
61
+ const widget = sdk.createFeedbackWidget({
62
+ position: 'bottom-right',
63
+ });
48
64
  widget.mount();
49
65
  ```
50
66
 
@@ -52,91 +68,117 @@ widget.mount();
52
68
 
53
69
  ## Installation Methods
54
70
 
55
- ### Method 1: NPM/ES Modules
71
+ ### Method 1: npm / ESM
56
72
 
57
73
  ```javascript
58
- import Product7 from '@product7/product7-js';
74
+ import { Product7 } from '@product7/product7-js';
59
75
 
60
76
  const sdk = new Product7({
61
- /* config */
77
+ workspace: 'your-workspace',
78
+ boardName: 'feature-requests',
62
79
  });
80
+
63
81
  await sdk.init();
82
+ await sdk.identify({
83
+ user_id: 'user_123',
84
+ email: 'user@example.com',
85
+ });
64
86
 
65
- const widget = sdk.createWidget('button');
87
+ const widget = sdk.createFeedbackWidget();
66
88
  widget.mount();
67
89
  ```
68
90
 
69
- ### Method 2: CDN with Auto-Init
91
+ ### Method 2: CDN
70
92
 
71
93
  ```html
94
+ <script src="https://cdn.jsdelivr.net/npm/@product7/product7-js@latest/dist/product7-js.min.js"></script>
72
95
  <script>
73
- window.Product7Config = {
74
- workspace: 'your-workspace',
75
- metadata: {
96
+ async function bootProduct7() {
97
+ const sdk = new window.Product7.Product7SDK({
98
+ workspace: 'your-workspace',
99
+ boardName: 'feature-requests',
100
+ });
101
+
102
+ await sdk.init();
103
+ await sdk.identify({
76
104
  user_id: 'user_123',
77
105
  email: 'user@example.com',
78
- },
79
- };
80
- </script>
81
- <script src="https://cdn.product7.io/product7-js/latest/product7-js.js"></script>
82
- <script>
83
- // SDK auto-initializes and is available at window.Product7
84
- window.Product7.onReady((sdk) => {
85
- const widget = sdk.createWidget('button');
106
+ });
107
+
108
+ const widget = sdk.createFeedbackWidget({
109
+ position: 'bottom-right',
110
+ });
86
111
  widget.mount();
87
- });
112
+ }
113
+
114
+ bootProduct7();
88
115
  </script>
89
116
  ```
90
117
 
91
- ### Method 3: Manual CDN
118
+ ### Method 3: Headless Feedback
92
119
 
93
- ```html
94
- <script src="https://cdn.product7.io/product7-js/latest/product7-js.js"></script>
95
- <script>
96
- const sdk = new window.Product7({
97
- workspace: 'your-workspace',
98
- metadata: {
99
- user_id: 'user_123',
100
- email: 'user@example.com',
101
- },
102
- });
120
+ Use `headless: true` when you want to control opening from your own UI instead of rendering the default floating trigger.
103
121
 
104
- sdk.init().then(() => {
105
- const widget = sdk.createWidget('button');
106
- widget.mount();
107
- });
108
- </script>
122
+ ```javascript
123
+ const widget = sdk.createFeedbackWidget({
124
+ headless: true,
125
+ boardName: 'feature-requests',
126
+ });
127
+
128
+ widget.mount();
129
+
130
+ document
131
+ .querySelector('#leave-feedback')
132
+ ?.addEventListener('click', () => widget.open());
109
133
  ```
110
134
 
111
135
  ---
112
136
 
113
- ## Framework Integration
137
+ ## Framework Examples
114
138
 
115
139
  ### React
116
140
 
117
141
  ```jsx
118
- import { useEffect, useState } from 'react';
119
- import Product7 from '@product7/product7-js';
142
+ import { useEffect, useRef } from 'react';
143
+ import { Product7 } from '@product7/product7-js';
120
144
 
121
145
  function App() {
122
- const [sdk, setSdk] = useState(null);
146
+ const sdkRef = useRef(null);
147
+ const widgetRef = useRef(null);
123
148
 
124
149
  useEffect(() => {
125
- const product7 = new Product7({
126
- workspace: 'your-workspace',
127
- metadata: {
128
- user_id: 'user_123',
129
- email: 'user@example.com',
130
- },
131
- });
150
+ let disposed = false;
132
151
 
133
- product7.init().then(() => {
134
- setSdk(product7);
135
- const widget = product7.createWidget('button');
136
- widget.mount();
137
- });
152
+ (async () => {
153
+ const sdk = new Product7({
154
+ workspace: 'your-workspace',
155
+ boardName: 'feature-requests',
156
+ });
138
157
 
139
- return () => product7.destroy();
158
+ await sdk.init();
159
+ await sdk.identify({
160
+ user_id: 'user_123',
161
+ email: 'user@example.com',
162
+ name: 'John Doe',
163
+ });
164
+
165
+ if (disposed) {
166
+ sdk.destroy();
167
+ return;
168
+ }
169
+
170
+ sdkRef.current = sdk;
171
+ widgetRef.current = sdk.createFeedbackWidget({
172
+ position: 'bottom-right',
173
+ });
174
+ widgetRef.current.mount();
175
+ })();
176
+
177
+ return () => {
178
+ disposed = true;
179
+ widgetRef.current?.destroy();
180
+ sdkRef.current?.destroy();
181
+ };
140
182
  }, []);
141
183
 
142
184
  return <div>Your App</div>;
@@ -147,93 +189,64 @@ function App() {
147
189
 
148
190
  ```vue
149
191
  <script setup>
150
- import { onMounted, onUnmounted } from 'vue';
151
- import Product7 from '@product7/product7-js';
192
+ import { onMounted, onUnmounted, ref } from 'vue';
193
+ import { Product7 } from '@product7/product7-js';
152
194
 
153
- let sdk = null;
195
+ const sdk = ref(null);
196
+ const widget = ref(null);
154
197
 
155
198
  onMounted(async () => {
156
- sdk = new Product7({
199
+ sdk.value = new Product7({
157
200
  workspace: 'your-workspace',
158
- metadata: {
159
- user_id: 'user_123',
160
- email: 'user@example.com',
161
- },
201
+ boardName: 'feature-requests',
162
202
  });
163
203
 
164
- await sdk.init();
165
- const widget = sdk.createWidget('button');
166
- widget.mount();
204
+ await sdk.value.init();
205
+ await sdk.value.identify({
206
+ user_id: 'user_123',
207
+ email: 'user@example.com',
208
+ name: 'John Doe',
209
+ });
210
+
211
+ widget.value = sdk.value.createFeedbackWidget({
212
+ position: 'bottom-right',
213
+ });
214
+ widget.value.mount();
167
215
  });
168
216
 
169
217
  onUnmounted(() => {
170
- if (sdk) sdk.destroy();
218
+ widget.value?.destroy();
219
+ sdk.value?.destroy();
171
220
  });
172
221
  </script>
173
-
174
- <template>
175
- <div>Your App</div>
176
- </template>
177
222
  ```
178
223
 
179
- ### Next.js
180
-
181
- ```javascript
182
- // app/layout.js or pages/_app.js
183
- 'use client';
184
-
185
- import { useEffect } from 'react';
186
- import Product7 from '@product7/product7-js';
187
-
188
- export default function RootLayout({ children }) {
189
- useEffect(() => {
190
- const sdk = new Product7({
191
- workspace: 'your-workspace',
192
- metadata: {
193
- user_id: 'user_123',
194
- email: 'user@example.com',
195
- },
196
- });
197
-
198
- sdk.init().then(() => {
199
- const widget = sdk.createWidget('button');
200
- widget.mount();
201
- });
202
-
203
- return () => sdk.destroy();
204
- }, []);
205
-
206
- return (
207
- <html>
208
- <body>{children}</body>
209
- </html>
210
- );
211
- }
212
- ```
213
-
214
- ---
215
-
216
- ## TypeScript Setup
217
-
218
- The SDK includes TypeScript definitions out of the box.
224
+ ### TypeScript
219
225
 
220
226
  ```typescript
221
- import Product7, { SDKConfig, ButtonWidget } from '@product7/product7-js';
227
+ import {
228
+ Product7,
229
+ type FeedbackConfig,
230
+ type FeedbackWidget,
231
+ } from '@product7/product7-js';
222
232
 
223
- const config: SDKConfig = {
233
+ const config: FeedbackConfig = {
224
234
  workspace: 'your-workspace',
225
- metadata: {
226
- user_id: 'user_123',
227
- email: 'user@example.com',
228
- name: 'John Doe',
229
- },
235
+ boardName: 'feature-requests',
230
236
  theme: 'light',
231
237
  };
232
238
 
233
239
  const sdk = new Product7(config);
234
240
  await sdk.init();
241
+ await sdk.identify({
242
+ user_id: 'user_123',
243
+ email: 'user@example.com',
244
+ name: 'John Doe',
245
+ });
235
246
 
236
- const widget: ButtonWidget = sdk.createWidget('button');
247
+ const widget: FeedbackWidget = sdk.createFeedbackWidget({
248
+ headless: true,
249
+ });
237
250
  widget.mount();
238
251
  ```
239
252
 
@@ -243,39 +256,54 @@ widget.mount();
243
256
 
244
257
  ### SDK not initializing
245
258
 
246
- Check you have both `workspace` and `metadata` configured:
259
+ Check that `workspace` is set:
247
260
 
248
261
  ```javascript
249
262
  const sdk = new Product7({
250
- workspace: 'your-workspace', // Required
251
- metadata: {
252
- // Required
253
- user_id: 'user_123', // Required: user_id or email
254
- email: 'user@example.com',
255
- },
263
+ workspace: 'your-workspace',
264
+ });
265
+ ```
266
+
267
+ ### Identify not working
268
+
269
+ Call `identify()` after `init()` and pass at least `user_id` or `email`:
270
+
271
+ ```javascript
272
+ await sdk.init();
273
+ await sdk.identify({
274
+ user_id: 'user_123',
275
+ email: 'user@example.com',
256
276
  });
257
277
  ```
258
278
 
259
279
  ### Widget not showing
260
280
 
261
- Ensure you call both `init()` and `mount()`:
281
+ Make sure you initialize first, then mount the widget:
282
+
283
+ ```javascript
284
+ await sdk.init();
285
+ const widget = sdk.createFeedbackWidget();
286
+ widget.mount();
287
+ ```
288
+
289
+ ### Custom trigger does nothing
290
+
291
+ If you use `headless: true`, you still need to call `mount()` before `open()`:
262
292
 
263
293
  ```javascript
264
- await sdk.init(); // Initialize first
265
- const widget = sdk.createWidget('button');
266
- widget.mount(); // Then mount
294
+ const widget = sdk.createFeedbackWidget({ headless: true });
295
+ widget.mount();
296
+ widget.open();
267
297
  ```
268
298
 
269
- ### CDN auto-init not working
299
+ ### CDN usage fails
270
300
 
271
- Check `window.Product7Config` is set before loading the script:
301
+ Use `window.Product7.Product7SDK` for the class-based API:
272
302
 
273
303
  ```html
274
- <!-- Config MUST come before script tag -->
275
304
  <script>
276
- window.Product7Config = {
277
- /* config */
278
- };
305
+ const sdk = new window.Product7.Product7SDK({
306
+ workspace: 'your-workspace',
307
+ });
279
308
  </script>
280
- <script src="https://cdn.product7.io/product7-js/latest/product7-js.js"></script>
281
309
  ```
package/src/index.js CHANGED
@@ -98,9 +98,21 @@ function cleanUndefined(obj) {
98
98
  return obj;
99
99
  }
100
100
 
101
+ function hasIdentifyMetadata(metadata = {}) {
102
+ return Object.values(metadata).some((value) => value !== undefined);
103
+ }
104
+
101
105
  // --- Ensure SDK is initialized (shared by widget inits) ---
102
106
 
103
107
  async function ensureSDK(options) {
108
+ if (
109
+ Product7._sdk &&
110
+ options.organization &&
111
+ Product7._sdk.config.workspace !== options.organization
112
+ ) {
113
+ Product7.destroy();
114
+ }
115
+
104
116
  if (Product7._sdk) return Product7._sdk;
105
117
 
106
118
  if (options.organization) {
@@ -143,63 +155,58 @@ const Product7 = {
143
155
  async identify(data = {}, callback) {
144
156
  try {
145
157
  const transformed = transformIdentifyData(data);
146
- Product7._organization = transformed.workspace;
158
+ Product7._organization =
159
+ transformed.workspace || Product7._organization || null;
147
160
 
148
161
  const config = cleanUndefined({
149
- workspace: transformed.workspace,
150
- metadata: transformed.metadata,
162
+ workspace: Product7._organization,
151
163
  debug: transformed.debug,
152
164
  mock: transformed.mock,
153
165
  env: transformed.env,
154
166
  apiUrl: transformed.apiUrl,
155
167
  });
156
168
 
157
- const sdk = new Product7SDK(config);
158
- const initData = await sdk.init();
159
-
160
- Product7._sdk = sdk;
161
- Product7._identified = true;
162
-
163
- // Sync custom attributes + segments via /widget/identify
164
- if (transformed.metadata && sdk.apiService?.sessionToken) {
165
- try {
166
- const identifyPayload = {
167
- user_id: transformed.metadata.user_id,
168
- email: transformed.metadata.email,
169
- name: transformed.metadata.name,
170
- avatar: transformed.metadata.profile_picture,
171
- attributes: transformed.metadata.custom_fields || {},
172
- };
173
- if (transformed.metadata.company) {
174
- identifyPayload.company = transformed.metadata.company;
175
- }
176
- await sdk.apiService._makeRequest('/widget/identify', {
177
- method: 'POST',
178
- body: JSON.stringify(identifyPayload),
179
- headers: {
180
- 'Content-Type': 'application/json',
181
- Authorization: `Bearer ${sdk.apiService.sessionToken}`,
182
- },
183
- });
184
- } catch (identifyErr) {
185
- if (config.debug) {
186
- console.warn('[Product7] Attribute sync failed:', identifyErr);
187
- }
169
+ let sdk = Product7._sdk;
170
+ const requiresNewSDK =
171
+ !sdk || (config.workspace && sdk.config.workspace !== config.workspace);
172
+
173
+ if (requiresNewSDK) {
174
+ if (Product7._sdk) {
175
+ Product7.destroy();
176
+ Product7._organization = config.workspace || null;
188
177
  }
178
+ sdk = new Product7SDK(config);
179
+ Product7._sdk = sdk;
189
180
  }
190
181
 
182
+ const initData = sdk.initialized
183
+ ? {
184
+ alreadyInitialized: true,
185
+ sessionToken: sdk.apiService?.sessionToken,
186
+ }
187
+ : await sdk.init();
188
+
189
+ const identifyData = hasIdentifyMetadata(transformed.metadata)
190
+ ? await sdk.identify(transformed.metadata)
191
+ : null;
192
+ Product7._identified = Boolean(identifyData?.identified);
193
+
191
194
  Product7._flushQueue();
192
195
 
193
196
  if (typeof window !== 'undefined' && typeof CustomEvent !== 'undefined') {
194
197
  window.dispatchEvent(
195
198
  new CustomEvent('Product7Ready', {
196
- detail: { sdk, config, initData },
199
+ detail: { sdk, config: sdk.config, initData, identifyData },
197
200
  })
198
201
  );
199
202
  }
200
203
 
201
204
  if (typeof callback === 'function') callback(null);
202
- return initData;
205
+ return {
206
+ ...initData,
207
+ identified: Product7._identified,
208
+ identify: identifyData,
209
+ };
203
210
  } catch (error) {
204
211
  console.error('[Product7] Identify failed:', error);
205
212
 
@@ -258,7 +265,7 @@ const Product7 = {
258
265
  if (!options.placement) {
259
266
  widgetOptions.autoShow = false;
260
267
  widgetOptions.displayMode = 'modal';
261
- widgetOptions._noTriggerButton = true;
268
+ widgetOptions.headless = true;
262
269
  } else {
263
270
  // Trigger button is always visible when placement is set
264
271
  widgetOptions.suppressAfterSubmission = false;
@@ -266,7 +273,7 @@ const Product7 = {
266
273
  }
267
274
 
268
275
  try {
269
- const widget = sdk.createWidget('button', widgetOptions);
276
+ const widget = sdk.createFeedbackWidget(widgetOptions);
270
277
  widget.mount();
271
278
 
272
279
  if (options.placement) {
@@ -305,13 +312,13 @@ const Product7 = {
305
312
  if (options.setBoard) {
306
313
  Product7._feedbackWidget.options.boardName = options.setBoard;
307
314
  }
308
- Product7._feedbackWidget.openPanel();
315
+ Product7._feedbackWidget.open();
309
316
  }
310
317
  },
311
318
 
312
319
  closeFeedback() {
313
320
  if (Product7._feedbackWidget) {
314
- Product7._feedbackWidget.closePanel();
321
+ Product7._feedbackWidget.close();
315
322
  }
316
323
  },
317
324
 
@@ -370,7 +377,7 @@ const Product7 = {
370
377
  widgetOptions.enabled = true;
371
378
 
372
379
  try {
373
- const widget = sdk.createWidget('messenger', widgetOptions);
380
+ const widget = sdk.createMessengerWidget(widgetOptions);
374
381
  widget.mount();
375
382
  widget.show();
376
383
 
@@ -100,6 +100,10 @@ export class MessengerWidget extends BaseWidget {
100
100
  this._handleConversationClosed = this._handleConversationClosed.bind(this);
101
101
  }
102
102
 
103
+ _hasTrigger() {
104
+ return this.options.trigger === true || this.options.trigger === undefined;
105
+ }
106
+
103
107
  _createInternalFeedbackWidget() {
104
108
  try {
105
109
  const widget = this.sdk.createWidget('button', {
@@ -135,11 +139,13 @@ export class MessengerWidget extends BaseWidget {
135
139
  this._feedbackWidget = this._createInternalFeedbackWidget();
136
140
  }
137
141
 
138
- this.launcher = new MessengerLauncher(this.messengerState, {
139
- position: this.messengerOptions.position,
140
- primaryColor: this.messengerOptions.primaryColor,
141
- });
142
- container.appendChild(this.launcher.render());
142
+ if (this._hasTrigger()) {
143
+ this.launcher = new MessengerLauncher(this.messengerState, {
144
+ position: this.messengerOptions.position,
145
+ primaryColor: this.messengerOptions.primaryColor,
146
+ });
147
+ container.appendChild(this.launcher.render());
148
+ }
143
149
 
144
150
  this.panel = new MessengerPanel(this.messengerState, {
145
151
  position: this.messengerOptions.position,
@@ -274,28 +280,13 @@ export class MessengerWidget extends BaseWidget {
274
280
 
275
281
  async _handleIdentifyContact(contactData) {
276
282
  try {
277
- const response = await this.apiService.identifyContact({
278
- name: contactData.name,
283
+ // Route through sdk.identify() so the SDK-level identity state is updated
284
+ // and applyIdentity() handles the messenger state + WebSocket as a side effect.
285
+ const result = await this.sdk.identify({
279
286
  email: contactData.email,
287
+ name: contactData.name,
280
288
  });
281
-
282
- if (response.status) {
283
- console.log(
284
- '[MessengerWidget] Contact identified:',
285
- response.data.contact_id
286
- );
287
- this.messengerState.setIdentified(true, {
288
- name: contactData.name,
289
- email: contactData.email,
290
- });
291
-
292
- // Start WebSocket now that session token is available
293
- if (this.apiService?.sessionToken && !this.wsService?.isConnected) {
294
- this._initWebSocket();
295
- }
296
- }
297
-
298
- return response;
289
+ return result;
299
290
  } catch (error) {
300
291
  console.error('[MessengerWidget] Failed to identify contact:', error);
301
292
  throw error;
@@ -303,10 +294,14 @@ export class MessengerWidget extends BaseWidget {
303
294
  }
304
295
 
305
296
  markAsIdentified(name, email) {
306
- this.messengerState.setIdentified(true, { name, email });
307
- console.log('[MessengerWidget] Marked as identified:', email);
297
+ // Called externally by the app when the user is already known.
298
+ // No API call needed — identity was already established via sdk.identify().
299
+ this.applyIdentity({ name, email });
300
+ }
301
+
302
+ applyIdentity(metadata = {}) {
303
+ this.messengerState.setIdentified(true, metadata);
308
304
 
309
- // Start WebSocket now that we have a session token
310
305
  if (this.apiService?.sessionToken && !this.wsService?.isConnected) {
311
306
  this._initWebSocket();
312
307
  }
@@ -850,7 +845,10 @@ export class MessengerWidget extends BaseWidget {
850
845
  }
851
846
 
852
847
  _hasExplicitOption(key) {
853
- return Object.prototype.hasOwnProperty.call(this._explicitOptions || {}, key);
848
+ return Object.prototype.hasOwnProperty.call(
849
+ this._explicitOptions || {},
850
+ key
851
+ );
854
852
  }
855
853
 
856
854
  async checkAgentAvailability() {