@feedvalue/core 0.1.9 → 0.1.10
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/dist/index.cjs +206 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +179 -3
- package/dist/index.d.ts +179 -3
- package/dist/index.js +206 -11
- package/dist/index.js.map +1 -1
- package/dist/umd/index.min.js +3 -3
- package/dist/umd/index.min.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -303,6 +303,84 @@ var ApiClient = class {
|
|
|
303
303
|
this.log("Feedback submitted", { feedbackId: data.feedback_id });
|
|
304
304
|
return data;
|
|
305
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Submit reaction
|
|
308
|
+
*/
|
|
309
|
+
async submitReaction(widgetId, reaction) {
|
|
310
|
+
this.validateWidgetId(widgetId);
|
|
311
|
+
const url = `${this.baseUrl}/api/v1/widgets/${widgetId}/react`;
|
|
312
|
+
if (!this.hasValidToken()) {
|
|
313
|
+
this.log("Token expired, refreshing...");
|
|
314
|
+
await this.fetchConfig(widgetId, true);
|
|
315
|
+
}
|
|
316
|
+
if (!this.submissionToken) {
|
|
317
|
+
throw new Error("No submission token available");
|
|
318
|
+
}
|
|
319
|
+
const headers = {
|
|
320
|
+
"Content-Type": "application/json",
|
|
321
|
+
"X-Submission-Token": this.submissionToken
|
|
322
|
+
};
|
|
323
|
+
if (this.fingerprint) {
|
|
324
|
+
headers["X-Client-Fingerprint"] = this.fingerprint;
|
|
325
|
+
}
|
|
326
|
+
this.log("Submitting reaction", { widgetId, value: reaction.value });
|
|
327
|
+
const response = await fetch(url, {
|
|
328
|
+
method: "POST",
|
|
329
|
+
headers,
|
|
330
|
+
body: JSON.stringify({
|
|
331
|
+
value: reaction.value,
|
|
332
|
+
followUp: reaction.followUp,
|
|
333
|
+
metadata: reaction.metadata
|
|
334
|
+
})
|
|
335
|
+
});
|
|
336
|
+
if (response.status === 429) {
|
|
337
|
+
const resetAt = response.headers.get("X-RateLimit-Reset");
|
|
338
|
+
const retryAfter = resetAt ? Math.ceil(parseInt(resetAt, 10) - Date.now() / 1e3) : 60;
|
|
339
|
+
throw new Error(`Rate limited. Try again in ${retryAfter} seconds.`);
|
|
340
|
+
}
|
|
341
|
+
if (response.status === 403) {
|
|
342
|
+
const errorData = await response.json().catch(() => ({ detail: "Access denied" }));
|
|
343
|
+
if (errorData.detail?.code && errorData.detail?.message) {
|
|
344
|
+
throw new Error(errorData.detail.message);
|
|
345
|
+
}
|
|
346
|
+
const errorMessage = typeof errorData.detail === "string" ? errorData.detail : "";
|
|
347
|
+
if (errorMessage.includes("token") || errorMessage.includes("expired")) {
|
|
348
|
+
this.log("Token rejected, refreshing...");
|
|
349
|
+
this.submissionToken = null;
|
|
350
|
+
await this.fetchConfig(widgetId, true);
|
|
351
|
+
if (this.submissionToken) {
|
|
352
|
+
headers["X-Submission-Token"] = this.submissionToken;
|
|
353
|
+
const retryResponse = await fetch(url, {
|
|
354
|
+
method: "POST",
|
|
355
|
+
headers,
|
|
356
|
+
body: JSON.stringify({
|
|
357
|
+
value: reaction.value,
|
|
358
|
+
followUp: reaction.followUp,
|
|
359
|
+
metadata: reaction.metadata
|
|
360
|
+
})
|
|
361
|
+
});
|
|
362
|
+
if (retryResponse.ok) {
|
|
363
|
+
return retryResponse.json();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
throw new Error(errorMessage || "Access denied");
|
|
368
|
+
}
|
|
369
|
+
if (response.status === 400) {
|
|
370
|
+
const error = await this.parseError(response);
|
|
371
|
+
throw new Error(error);
|
|
372
|
+
}
|
|
373
|
+
if (!response.ok) {
|
|
374
|
+
const error = await this.parseError(response);
|
|
375
|
+
throw new Error(error);
|
|
376
|
+
}
|
|
377
|
+
const data = await response.json();
|
|
378
|
+
if (data.blocked) {
|
|
379
|
+
throw new Error(data.message || "Unable to submit reaction");
|
|
380
|
+
}
|
|
381
|
+
this.log("Reaction submitted", { submissionId: data.submission_id });
|
|
382
|
+
return data;
|
|
383
|
+
}
|
|
306
384
|
/**
|
|
307
385
|
* Parse error from response
|
|
308
386
|
*/
|
|
@@ -482,20 +560,27 @@ var _FeedValue = class _FeedValue {
|
|
|
482
560
|
this.log("Instance destroyed during config fetch, aborting init");
|
|
483
561
|
return;
|
|
484
562
|
}
|
|
563
|
+
const baseConfig = {
|
|
564
|
+
position: configResponse.config.position ?? "bottom-right",
|
|
565
|
+
triggerText: configResponse.config.triggerText ?? "Feedback",
|
|
566
|
+
triggerIcon: configResponse.config.triggerIcon ?? "none",
|
|
567
|
+
formTitle: configResponse.config.formTitle ?? "Share your feedback",
|
|
568
|
+
submitButtonText: configResponse.config.submitButtonText ?? "Submit",
|
|
569
|
+
thankYouMessage: configResponse.config.thankYouMessage ?? "Thank you for your feedback!",
|
|
570
|
+
showBranding: configResponse.config.showBranding ?? true,
|
|
571
|
+
customFields: configResponse.config.customFields,
|
|
572
|
+
// Reaction config (for reaction widgets) - only include if defined
|
|
573
|
+
...configResponse.config.template && { template: configResponse.config.template },
|
|
574
|
+
...configResponse.config.options && { options: configResponse.config.options },
|
|
575
|
+
followUpLabel: configResponse.config.followUpLabel ?? "Tell us more (optional)",
|
|
576
|
+
submitText: configResponse.config.submitText ?? "Send"
|
|
577
|
+
};
|
|
485
578
|
this.widgetConfig = {
|
|
486
579
|
widgetId: configResponse.widget_id,
|
|
487
580
|
widgetKey: configResponse.widget_key,
|
|
488
581
|
appId: "",
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
triggerText: configResponse.config.triggerText ?? "Feedback",
|
|
492
|
-
triggerIcon: configResponse.config.triggerIcon ?? "none",
|
|
493
|
-
formTitle: configResponse.config.formTitle ?? "Share your feedback",
|
|
494
|
-
submitButtonText: configResponse.config.submitButtonText ?? "Submit",
|
|
495
|
-
thankYouMessage: configResponse.config.thankYouMessage ?? "Thank you for your feedback!",
|
|
496
|
-
showBranding: configResponse.config.showBranding ?? true,
|
|
497
|
-
customFields: configResponse.config.customFields
|
|
498
|
-
},
|
|
582
|
+
type: configResponse.type ?? "feedback",
|
|
583
|
+
config: baseConfig,
|
|
499
584
|
styling: {
|
|
500
585
|
primaryColor: configResponse.styling.primaryColor ?? "#3b82f6",
|
|
501
586
|
backgroundColor: configResponse.styling.backgroundColor ?? "#ffffff",
|
|
@@ -713,6 +798,108 @@ var _FeedValue = class _FeedValue {
|
|
|
713
798
|
}
|
|
714
799
|
return result;
|
|
715
800
|
}
|
|
801
|
+
// ===========================================================================
|
|
802
|
+
// Reactions
|
|
803
|
+
// ===========================================================================
|
|
804
|
+
/**
|
|
805
|
+
* Get reaction options from widget config.
|
|
806
|
+
* Returns null if widget is not a reaction type.
|
|
807
|
+
*/
|
|
808
|
+
getReactionOptions() {
|
|
809
|
+
if (!this.widgetConfig || this.widgetConfig.type !== "reaction") {
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
const config = this.widgetConfig.config;
|
|
813
|
+
if (config.template) {
|
|
814
|
+
return this.getTemplateOptions(config.template);
|
|
815
|
+
}
|
|
816
|
+
return config.options ?? null;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Get predefined options for a reaction template
|
|
820
|
+
*/
|
|
821
|
+
getTemplateOptions(template) {
|
|
822
|
+
const templates = {
|
|
823
|
+
thumbs: [
|
|
824
|
+
{ label: "Helpful", value: "helpful", icon: "thumbs-up", showFollowUp: false },
|
|
825
|
+
{ label: "Not Helpful", value: "not_helpful", icon: "thumbs-down", showFollowUp: true }
|
|
826
|
+
],
|
|
827
|
+
helpful: [
|
|
828
|
+
{ label: "Yes", value: "yes", icon: "check", showFollowUp: false },
|
|
829
|
+
{ label: "No", value: "no", icon: "x", showFollowUp: true }
|
|
830
|
+
],
|
|
831
|
+
emoji: [
|
|
832
|
+
{ label: "Angry", value: "angry", icon: "\u{1F620}", showFollowUp: true },
|
|
833
|
+
{ label: "Disappointed", value: "disappointed", icon: "\u{1F61E}", showFollowUp: true },
|
|
834
|
+
{ label: "Neutral", value: "neutral", icon: "\u{1F610}", showFollowUp: false },
|
|
835
|
+
{ label: "Satisfied", value: "satisfied", icon: "\u{1F60A}", showFollowUp: false },
|
|
836
|
+
{ label: "Excited", value: "excited", icon: "\u{1F60D}", showFollowUp: false }
|
|
837
|
+
],
|
|
838
|
+
rating: [
|
|
839
|
+
{ label: "1", value: "1", icon: "\u2B50", showFollowUp: true },
|
|
840
|
+
{ label: "2", value: "2", icon: "\u2B50\u2B50", showFollowUp: true },
|
|
841
|
+
{ label: "3", value: "3", icon: "\u2B50\u2B50\u2B50", showFollowUp: false },
|
|
842
|
+
{ label: "4", value: "4", icon: "\u2B50\u2B50\u2B50\u2B50", showFollowUp: false },
|
|
843
|
+
{ label: "5", value: "5", icon: "\u2B50\u2B50\u2B50\u2B50\u2B50", showFollowUp: false }
|
|
844
|
+
]
|
|
845
|
+
};
|
|
846
|
+
return templates[template] ?? [];
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Submit a reaction.
|
|
850
|
+
* @param value - Selected reaction option value
|
|
851
|
+
* @param options - Optional follow-up text
|
|
852
|
+
*/
|
|
853
|
+
async react(value, options) {
|
|
854
|
+
if (!this.state.isReady) {
|
|
855
|
+
throw new Error("Widget not ready");
|
|
856
|
+
}
|
|
857
|
+
if (!this.widgetConfig || this.widgetConfig.type !== "reaction") {
|
|
858
|
+
throw new Error("This is not a reaction widget");
|
|
859
|
+
}
|
|
860
|
+
const reactionOptions = this.getReactionOptions();
|
|
861
|
+
if (!reactionOptions) {
|
|
862
|
+
throw new Error("No reaction options configured");
|
|
863
|
+
}
|
|
864
|
+
const selectedOption = reactionOptions.find((opt) => opt.value === value);
|
|
865
|
+
if (!selectedOption) {
|
|
866
|
+
const validValues = reactionOptions.map((opt) => opt.value).join(", ");
|
|
867
|
+
throw new Error(`Invalid reaction value. Must be one of: ${validValues}`);
|
|
868
|
+
}
|
|
869
|
+
this.emitter.emit("react", { value, hasFollowUp: selectedOption.showFollowUp });
|
|
870
|
+
this.updateState({ isSubmitting: true });
|
|
871
|
+
try {
|
|
872
|
+
const reactionData = {
|
|
873
|
+
value,
|
|
874
|
+
metadata: {
|
|
875
|
+
page_url: typeof window !== "undefined" ? window.location.href : ""
|
|
876
|
+
},
|
|
877
|
+
...options?.followUp && { followUp: options.followUp }
|
|
878
|
+
};
|
|
879
|
+
await this.apiClient.submitReaction(this.widgetId, reactionData);
|
|
880
|
+
const emitData = options?.followUp ? { value, followUp: options.followUp } : { value };
|
|
881
|
+
this.emitter.emit("reactSubmit", emitData);
|
|
882
|
+
this.log("Reaction submitted", { value });
|
|
883
|
+
} catch (error) {
|
|
884
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
885
|
+
this.emitter.emit("reactError", err);
|
|
886
|
+
throw err;
|
|
887
|
+
} finally {
|
|
888
|
+
this.updateState({ isSubmitting: false });
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Check if widget is a reaction type
|
|
893
|
+
*/
|
|
894
|
+
isReaction() {
|
|
895
|
+
return this.widgetConfig?.type === "reaction";
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Get widget type ('feedback' or 'reaction')
|
|
899
|
+
*/
|
|
900
|
+
getWidgetType() {
|
|
901
|
+
return this.widgetConfig?.type ?? "feedback";
|
|
902
|
+
}
|
|
716
903
|
/**
|
|
717
904
|
* Validate feedback data before submission
|
|
718
905
|
* @throws Error if validation fails
|
|
@@ -1297,9 +1484,18 @@ __publicField(_FeedValue, "TRIGGER_ICONS", {
|
|
|
1297
1484
|
});
|
|
1298
1485
|
var FeedValue = _FeedValue;
|
|
1299
1486
|
|
|
1487
|
+
// src/types.ts
|
|
1488
|
+
var NEGATIVE_OPTIONS_MAP = {
|
|
1489
|
+
thumbs: ["not_helpful"],
|
|
1490
|
+
helpful: ["no"],
|
|
1491
|
+
emoji: ["angry", "disappointed"],
|
|
1492
|
+
rating: ["1", "2"]
|
|
1493
|
+
};
|
|
1494
|
+
|
|
1300
1495
|
exports.ApiClient = ApiClient;
|
|
1301
1496
|
exports.DEFAULT_API_BASE_URL = DEFAULT_API_BASE_URL;
|
|
1302
1497
|
exports.FeedValue = FeedValue;
|
|
1498
|
+
exports.NEGATIVE_OPTIONS_MAP = NEGATIVE_OPTIONS_MAP;
|
|
1303
1499
|
exports.TypedEventEmitter = TypedEventEmitter;
|
|
1304
1500
|
exports.clearFingerprint = clearFingerprint;
|
|
1305
1501
|
exports.generateFingerprint = generateFingerprint;
|