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