@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 +103 -10
- package/dist/index.cjs +52 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -4
- package/dist/index.d.ts +10 -4
- package/dist/index.js +52 -31
- package/dist/index.js.map +1 -1
- package/dist/umd/index.min.js +2 -2
- package/dist/umd/index.min.js.map +1 -1
- package/package.json +12 -13
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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", {
|
|
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:
|
|
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
|
|
318
|
-
if (typeof crypto
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
|
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 =
|
|
364
|
+
const fingerprint = generateHexFingerprint();
|
|
344
365
|
try {
|
|
345
366
|
sessionStorage.setItem(FINGERPRINT_STORAGE_KEY, fingerprint);
|
|
346
367
|
} catch {
|