@feedvalue/core 0.1.0 → 0.1.2

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 CHANGED
@@ -35,8 +35,30 @@ feedvalue.open();
35
35
  feedvalue.close();
36
36
  ```
37
37
 
38
+ ### Headless Mode
39
+
40
+ For complete UI control, initialize in headless mode. The SDK fetches config and provides all API methods but renders no DOM elements:
41
+
42
+ ```typescript
43
+ const feedvalue = FeedValue.init({
44
+ widgetId: 'your-widget-id',
45
+ headless: true, // No trigger button or modal rendered
46
+ });
47
+
48
+ // Wait until ready
49
+ await feedvalue.waitUntilReady();
50
+
51
+ // Build your own UI, use SDK for submission
52
+ await feedvalue.submit({
53
+ message: 'User feedback here',
54
+ sentiment: 'satisfied',
55
+ });
56
+ ```
57
+
38
58
  ### User Identification
39
59
 
60
+ User data is automatically included with feedback submissions:
61
+
40
62
  ```typescript
41
63
  // Identify the current user
42
64
  feedvalue.identify('user-123', {
@@ -61,14 +83,47 @@ feedvalue.reset();
61
83
  // Submit feedback programmatically
62
84
  await feedvalue.submit({
63
85
  message: 'Great product!',
64
- sentiment: '😊',
86
+ sentiment: 'excited', // 'angry' | 'disappointed' | 'satisfied' | 'excited'
65
87
  metadata: {
66
88
  page_url: window.location.href,
67
- custom_field: 'value',
68
89
  },
69
90
  });
70
91
  ```
71
92
 
93
+ ### Custom Fields
94
+
95
+ Custom fields allow you to collect structured data beyond the main feedback message. **Custom fields must be defined in your widget configuration on the FeedValue dashboard before use.**
96
+
97
+ 1. Go to your widget settings in the FeedValue dashboard
98
+ 2. Add custom fields with types: `text`, `email`, or `emoji`
99
+ 3. Use `customFieldValues` to submit responses:
100
+
101
+ ```typescript
102
+ await feedvalue.submit({
103
+ message: 'Detailed feedback',
104
+ customFieldValues: {
105
+ // Field IDs must match those defined in your widget configuration
106
+ name: 'John Doe',
107
+ category: 'feature',
108
+ },
109
+ });
110
+ ```
111
+
112
+ > **Important**: The field IDs in `customFieldValues` must match the field IDs defined in your widget configuration on the dashboard.
113
+
114
+ ### Waiting for Ready State
115
+
116
+ ```typescript
117
+ // Wait until widget is fully initialized
118
+ await feedvalue.waitUntilReady();
119
+ console.log('Widget ready, config loaded');
120
+
121
+ // Or use the event
122
+ feedvalue.on('ready', () => {
123
+ console.log('Widget is ready');
124
+ });
125
+ ```
126
+
72
127
  ### Event Handling
73
128
 
74
129
  ```typescript
@@ -93,6 +148,11 @@ feedvalue.on('error', (error) => {
93
148
  console.error('Widget error:', error);
94
149
  });
95
150
 
151
+ // Subscribe to a single event emission
152
+ feedvalue.once('ready', () => {
153
+ console.log('First ready event only');
154
+ });
155
+
96
156
  // Unsubscribe from events
97
157
  feedvalue.off('open', handleOpen);
98
158
  ```
@@ -111,17 +171,32 @@ const state = feedvalue.getSnapshot();
111
171
  // { isReady, isOpen, isVisible, error, isSubmitting }
112
172
  ```
113
173
 
174
+ ### Configuration Updates
175
+
176
+ ```typescript
177
+ // Update runtime configuration
178
+ feedvalue.setConfig({
179
+ theme: 'dark',
180
+ debug: true,
181
+ });
182
+
183
+ // Get current configuration
184
+ const config = feedvalue.getConfig();
185
+ ```
186
+
114
187
  ## API Reference
115
188
 
116
189
  ### `FeedValue.init(options)`
117
190
 
118
191
  Initialize a FeedValue instance.
119
192
 
120
- | Option | Type | Required | Description |
121
- |--------|------|----------|-------------|
122
- | `widgetId` | `string` | Yes | Widget ID from FeedValue dashboard |
123
- | `apiBaseUrl` | `string` | No | Custom API URL (for self-hosted) |
124
- | `config` | `Partial<FeedValueConfig>` | No | Configuration overrides |
193
+ | Option | Type | Required | Default | Description |
194
+ |--------|------|----------|---------|-------------|
195
+ | `widgetId` | `string` | Yes | - | Widget ID from FeedValue dashboard |
196
+ | `apiBaseUrl` | `string` | No | Production URL | Custom API URL (for self-hosted) |
197
+ | `config` | `Partial<FeedValueConfig>` | No | - | Configuration overrides |
198
+ | `headless` | `boolean` | No | `false` | Disable all DOM rendering |
199
+ | `debug` | `boolean` | No | `false` | Enable debug logging |
125
200
 
126
201
  ### Instance Methods
127
202
 
@@ -132,23 +207,41 @@ Initialize a FeedValue instance.
132
207
  | `toggle()` | Toggle the modal open/closed |
133
208
  | `show()` | Show the trigger button |
134
209
  | `hide()` | Hide the trigger button |
210
+ | `isOpen()` | Check if modal is open |
211
+ | `isVisible()` | Check if trigger is visible |
212
+ | `isReady()` | Check if widget is ready |
213
+ | `isHeadless()` | Check if running in headless mode |
135
214
  | `submit(feedback)` | Submit feedback programmatically |
136
215
  | `identify(userId, traits?)` | Identify the current user |
137
216
  | `setData(data)` | Set additional user data |
138
217
  | `reset()` | Reset user data |
139
218
  | `on(event, handler)` | Subscribe to events |
219
+ | `once(event, handler)` | Subscribe to single event |
140
220
  | `off(event, handler?)` | Unsubscribe from events |
221
+ | `waitUntilReady()` | Promise that resolves when ready |
141
222
  | `subscribe(callback)` | Subscribe to state changes |
142
223
  | `getSnapshot()` | Get current state |
224
+ | `setConfig(config)` | Update runtime configuration |
143
225
  | `getConfig()` | Get current configuration |
144
226
  | `destroy()` | Destroy the widget instance |
145
227
 
228
+ ### Events
229
+
230
+ | Event | Payload | Description |
231
+ |-------|---------|-------------|
232
+ | `ready` | - | Widget initialized, config loaded |
233
+ | `open` | - | Modal opened |
234
+ | `close` | - | Modal closed |
235
+ | `submit` | `FeedbackData` | Feedback submitted successfully |
236
+ | `error` | `Error` | An error occurred |
237
+ | `stateChange` | `FeedValueState` | Any state change |
238
+
146
239
  ## Framework Packages
147
240
 
148
- For framework-specific integrations:
241
+ For framework-specific integrations with hooks and components:
149
242
 
150
- - **React**: [@feedvalue/react](https://www.npmjs.com/package/@feedvalue/react)
151
- - **Vue**: [@feedvalue/vue](https://www.npmjs.com/package/@feedvalue/vue)
243
+ - **React/Next.js**: [@feedvalue/react](https://www.npmjs.com/package/@feedvalue/react)
244
+ - **Vue/Nuxt**: [@feedvalue/vue](https://www.npmjs.com/package/@feedvalue/vue)
152
245
 
153
246
  ## License
154
247
 
package/dist/index.cjs CHANGED
@@ -140,14 +140,21 @@ var ApiClient = class {
140
140
  /**
141
141
  * Fetch widget configuration
142
142
  * Uses caching and request deduplication
143
+ *
144
+ * @param widgetId - Widget ID to fetch config for
145
+ * @param forceRefresh - Skip cache and fetch fresh config (used for token refresh)
143
146
  */
144
- async fetchConfig(widgetId) {
147
+ async fetchConfig(widgetId, forceRefresh = false) {
145
148
  this.validateWidgetId(widgetId);
146
149
  const cacheKey = `config:${widgetId}`;
147
- const cached = this.configCache.get(cacheKey);
148
- if (cached && Date.now() < cached.expiresAt) {
149
- this.log("Config cache hit", { widgetId });
150
- return cached.data;
150
+ if (!forceRefresh) {
151
+ const cached = this.configCache.get(cacheKey);
152
+ if (cached && Date.now() < cached.expiresAt) {
153
+ this.log("Config cache hit", { widgetId });
154
+ return cached.data;
155
+ }
156
+ } else {
157
+ this.log("Bypassing cache for token refresh", { widgetId });
151
158
  }
152
159
  const pendingKey = `fetchConfig:${widgetId}`;
153
160
  const pending = this.pendingRequests.get(pendingKey);
@@ -173,7 +180,12 @@ var ApiClient = class {
173
180
  if (this.fingerprint) {
174
181
  headers["X-Client-Fingerprint"] = this.fingerprint;
175
182
  }
176
- this.log("Fetching config", { widgetId, url });
183
+ this.log("Fetching config", {
184
+ widgetId,
185
+ url,
186
+ hasFingerprint: !!this.fingerprint,
187
+ fingerprintPreview: this.fingerprint ? `${this.fingerprint.substring(0, 8)}...` : null
188
+ });
177
189
  const response = await fetch(url, {
178
190
  method: "GET",
179
191
  headers
@@ -189,6 +201,12 @@ var ApiClient = class {
189
201
  this.log("Submission token stored", {
190
202
  expiresAt: this.tokenExpiresAt ? new Date(this.tokenExpiresAt * 1e3).toISOString() : "unknown"
191
203
  });
204
+ } else {
205
+ this.log("No submission token in response", {
206
+ hasWidgetId: !!data.widget_id,
207
+ hasConfig: !!data.config,
208
+ responseKeys: Object.keys(data)
209
+ });
192
210
  }
193
211
  const cacheKey = `config:${widgetId}`;
194
212
  this.configCache.set(cacheKey, {
@@ -206,7 +224,7 @@ var ApiClient = class {
206
224
  const url = `${this.baseUrl}/api/v1/widgets/${widgetId}/feedback`;
207
225
  if (!this.hasValidToken()) {
208
226
  this.log("Token expired, refreshing...");
209
- await this.fetchConfig(widgetId);
227
+ await this.fetchConfig(widgetId, true);
210
228
  }
211
229
  if (!this.submissionToken) {
212
230
  throw new Error("No submission token available");
@@ -219,17 +237,20 @@ var ApiClient = class {
219
237
  headers["X-Client-Fingerprint"] = this.fingerprint;
220
238
  }
221
239
  this.log("Submitting feedback", { widgetId });
240
+ const mergedMetadata = {
241
+ ...feedback.metadata,
242
+ ...userData && Object.keys(userData).length > 0 && {
243
+ user: userData
244
+ }
245
+ };
222
246
  const response = await fetch(url, {
223
247
  method: "POST",
224
248
  headers,
225
249
  body: JSON.stringify({
226
250
  message: feedback.message,
227
- metadata: feedback.metadata,
251
+ metadata: Object.keys(mergedMetadata).length > 0 ? mergedMetadata : void 0,
228
252
  ...feedback.customFieldValues && {
229
253
  customFieldValues: feedback.customFieldValues
230
- },
231
- ...userData && Object.keys(userData).length > 0 && {
232
- user: userData
233
254
  }
234
255
  })
235
256
  });
@@ -247,7 +268,7 @@ var ApiClient = class {
247
268
  if (errorMessage.includes("token") || errorMessage.includes("expired")) {
248
269
  this.log("Token rejected, refreshing...");
249
270
  this.submissionToken = null;
250
- await this.fetchConfig(widgetId);
271
+ await this.fetchConfig(widgetId, true);
251
272
  if (this.submissionToken) {
252
273
  headers["X-Submission-Token"] = this.submissionToken;
253
274
  const retryResponse = await fetch(url, {
@@ -314,33 +335,33 @@ var ApiClient = class {
314
335
 
315
336
  // src/fingerprint.ts
316
337
  var FINGERPRINT_STORAGE_KEY = "fv_fingerprint";
317
- function generateUUID() {
318
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
319
- return crypto.randomUUID();
320
- }
321
- if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
322
- const bytes = new Uint8Array(16);
323
- crypto.getRandomValues(bytes);
324
- bytes[6] = bytes[6] & 15 | 64;
325
- bytes[8] = bytes[8] & 63 | 128;
326
- const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
327
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
328
- }
329
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
330
- const r = Math.random() * 16 | 0;
331
- const v = c === "x" ? r : r & 3 | 8;
332
- return v.toString(16);
333
- });
338
+ function generateHexFingerprint() {
339
+ if (typeof crypto === "undefined" || typeof crypto.getRandomValues !== "function") {
340
+ throw new Error(
341
+ "crypto.getRandomValues is required but not available. Ensure you are running in a modern browser or Node.js 15+."
342
+ );
343
+ }
344
+ const bytes = new Uint8Array(16);
345
+ crypto.getRandomValues(bytes);
346
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
334
347
  }
335
348
  function generateFingerprint() {
336
349
  if (typeof window === "undefined" || typeof sessionStorage === "undefined") {
337
- return generateUUID();
350
+ return generateHexFingerprint();
338
351
  }
339
352
  const stored = sessionStorage.getItem(FINGERPRINT_STORAGE_KEY);
340
353
  if (stored) {
354
+ if (stored.includes("-")) {
355
+ const hexFingerprint = stored.replace(/-/g, "");
356
+ try {
357
+ sessionStorage.setItem(FINGERPRINT_STORAGE_KEY, hexFingerprint);
358
+ } catch {
359
+ }
360
+ return hexFingerprint;
361
+ }
341
362
  return stored;
342
363
  }
343
- const fingerprint = generateUUID();
364
+ const fingerprint = generateHexFingerprint();
344
365
  try {
345
366
  sessionStorage.setItem(FINGERPRINT_STORAGE_KEY, fingerprint);
346
367
  } catch {