@feedvalue/core 0.1.9 → 0.1.11

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 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
- config: {
490
- position: configResponse.config.position ?? "bottom-right",
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;