@usero/sdk 0.2.1 → 0.3.1

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.
@@ -1,3 +1,23 @@
1
+ interface PluginLogger {
2
+ debug: (...args: unknown[]) => void;
3
+ info: (...args: unknown[]) => void;
4
+ warn: (...args: unknown[]) => void;
5
+ error: (...args: unknown[]) => void;
6
+ }
7
+ interface PluginContext {
8
+ clientId: string;
9
+ baseUrl: string;
10
+ getStore: <T>() => T | undefined;
11
+ setStore: <T>(value: T) => void;
12
+ logger: PluginLogger;
13
+ }
14
+ interface UseroPlugin {
15
+ name: string;
16
+ onInit?: (ctx: PluginContext) => void | Promise<void>;
17
+ onFeedbackSubmit?: (ctx: PluginContext, submission: FeedbackSubmission) => Promise<Partial<FeedbackSubmission> | undefined> | Partial<FeedbackSubmission> | undefined;
18
+ onDestroy?: (ctx: PluginContext) => void;
19
+ }
20
+
1
21
  type FeedbackRating = 1 | 2 | 3 | 4;
2
22
  interface FeedbackMetadata {
3
23
  pageUrl: string;
@@ -24,6 +44,7 @@ interface FeedbackSubmission {
24
44
  environment?: string;
25
45
  screenshots?: ScreenshotData[];
26
46
  metadata?: Record<string, unknown>;
47
+ replayEvents?: string;
27
48
  }
28
49
  interface FeedbackData {
29
50
  rating?: FeedbackRating;
@@ -51,6 +72,7 @@ interface FeedbackWidgetProps {
51
72
  environment?: string;
52
73
  baseUrl?: string;
53
74
  metadata?: Record<string, unknown>;
75
+ plugins?: ReadonlyArray<UseroPlugin>;
54
76
  onSubmit?: (feedback: FeedbackData) => void;
55
77
  onError?: (error: Error) => void;
56
78
  onOpen?: () => void;
@@ -67,7 +89,9 @@ interface UseroWidgetHandle {
67
89
  open: () => void;
68
90
  close: () => void;
69
91
  update: (next: Partial<Omit<FeedbackWidgetProps, 'clientId' | 'baseUrl'>>) => void;
92
+ whenReady: () => Promise<void>;
70
93
  }
94
+ declare function mergePluginPatches(base: FeedbackSubmission, patches: ReadonlyArray<Partial<FeedbackSubmission> | undefined>): FeedbackSubmission;
71
95
  declare function initUseroFeedbackWidget(props: FeedbackWidgetProps): UseroWidgetHandle;
72
96
 
73
- export { DARK_THEME, DEFAULT_THEME, type FeedbackData, type FeedbackRating, type FeedbackSubmission, type FeedbackWidgetProps, type ScreenshotData, type UseroWidgetHandle, type WidgetPosition, type WidgetTheme, initUseroFeedbackWidget, mergeTheme, resolveTheme };
97
+ export { DARK_THEME, DEFAULT_THEME, type FeedbackData, type FeedbackRating, type FeedbackSubmission, type FeedbackWidgetProps, type PluginContext, type PluginLogger, type ScreenshotData, type UseroPlugin, type UseroWidgetHandle, type WidgetPosition, type WidgetTheme, initUseroFeedbackWidget, mergePluginPatches, mergeTheme, resolveTheme };
package/dist/vanilla.d.ts CHANGED
@@ -1,3 +1,23 @@
1
+ interface PluginLogger {
2
+ debug: (...args: unknown[]) => void;
3
+ info: (...args: unknown[]) => void;
4
+ warn: (...args: unknown[]) => void;
5
+ error: (...args: unknown[]) => void;
6
+ }
7
+ interface PluginContext {
8
+ clientId: string;
9
+ baseUrl: string;
10
+ getStore: <T>() => T | undefined;
11
+ setStore: <T>(value: T) => void;
12
+ logger: PluginLogger;
13
+ }
14
+ interface UseroPlugin {
15
+ name: string;
16
+ onInit?: (ctx: PluginContext) => void | Promise<void>;
17
+ onFeedbackSubmit?: (ctx: PluginContext, submission: FeedbackSubmission) => Promise<Partial<FeedbackSubmission> | undefined> | Partial<FeedbackSubmission> | undefined;
18
+ onDestroy?: (ctx: PluginContext) => void;
19
+ }
20
+
1
21
  type FeedbackRating = 1 | 2 | 3 | 4;
2
22
  interface FeedbackMetadata {
3
23
  pageUrl: string;
@@ -24,6 +44,7 @@ interface FeedbackSubmission {
24
44
  environment?: string;
25
45
  screenshots?: ScreenshotData[];
26
46
  metadata?: Record<string, unknown>;
47
+ replayEvents?: string;
27
48
  }
28
49
  interface FeedbackData {
29
50
  rating?: FeedbackRating;
@@ -51,6 +72,7 @@ interface FeedbackWidgetProps {
51
72
  environment?: string;
52
73
  baseUrl?: string;
53
74
  metadata?: Record<string, unknown>;
75
+ plugins?: ReadonlyArray<UseroPlugin>;
54
76
  onSubmit?: (feedback: FeedbackData) => void;
55
77
  onError?: (error: Error) => void;
56
78
  onOpen?: () => void;
@@ -67,7 +89,9 @@ interface UseroWidgetHandle {
67
89
  open: () => void;
68
90
  close: () => void;
69
91
  update: (next: Partial<Omit<FeedbackWidgetProps, 'clientId' | 'baseUrl'>>) => void;
92
+ whenReady: () => Promise<void>;
70
93
  }
94
+ declare function mergePluginPatches(base: FeedbackSubmission, patches: ReadonlyArray<Partial<FeedbackSubmission> | undefined>): FeedbackSubmission;
71
95
  declare function initUseroFeedbackWidget(props: FeedbackWidgetProps): UseroWidgetHandle;
72
96
 
73
- export { DARK_THEME, DEFAULT_THEME, type FeedbackData, type FeedbackRating, type FeedbackSubmission, type FeedbackWidgetProps, type ScreenshotData, type UseroWidgetHandle, type WidgetPosition, type WidgetTheme, initUseroFeedbackWidget, mergeTheme, resolveTheme };
97
+ export { DARK_THEME, DEFAULT_THEME, type FeedbackData, type FeedbackRating, type FeedbackSubmission, type FeedbackWidgetProps, type PluginContext, type PluginLogger, type ScreenshotData, type UseroPlugin, type UseroWidgetHandle, type WidgetPosition, type WidgetTheme, initUseroFeedbackWidget, mergePluginPatches, mergeTheme, resolveTheme };
package/dist/vanilla.js CHANGED
@@ -155,6 +155,25 @@ function getGradientEnd(color) {
155
155
  return `#${[shiftedR, shiftedG, shiftedB].map((x) => x.toString(16).padStart(2, "0")).join("")}`;
156
156
  }
157
157
 
158
+ // src/plugin.ts
159
+ function createPluginLogger(name) {
160
+ const prefix = `[usero:${name}]`;
161
+ return {
162
+ debug: (...args) => {
163
+ if (typeof console !== "undefined") console.debug(prefix, ...args);
164
+ },
165
+ info: (...args) => {
166
+ if (typeof console !== "undefined") console.info(prefix, ...args);
167
+ },
168
+ warn: (...args) => {
169
+ if (typeof console !== "undefined") console.warn(prefix, ...args);
170
+ },
171
+ error: (...args) => {
172
+ if (typeof console !== "undefined") console.error(prefix, ...args);
173
+ }
174
+ };
175
+ }
176
+
158
177
  // src/validation.ts
159
178
  function validateFeedbackSubmission(data) {
160
179
  const errors = [];
@@ -335,10 +354,17 @@ var FEEDBACK_CSS = `
335
354
  box-sizing: border-box;
336
355
  }
337
356
 
357
+ .fb-toolrow {
358
+ display: flex;
359
+ align-items: center;
360
+ justify-content: space-between;
361
+ gap: 12px;
362
+ margin-bottom: 8px;
363
+ }
364
+
338
365
  .fb-charcount {
339
366
  font-size: 12px;
340
367
  margin-left: auto;
341
- margin-bottom: 6px;
342
368
  text-align: right;
343
369
  }
344
370
 
@@ -634,6 +660,21 @@ function escapeHtml(value) {
634
660
  }
635
661
  });
636
662
  }
663
+ function mergePluginPatches(base, patches) {
664
+ let result = base;
665
+ for (const patch of patches) {
666
+ if (!patch || typeof patch !== "object") continue;
667
+ const { metadata: patchMetadata, ...rest } = patch;
668
+ result = { ...result, ...rest };
669
+ if (patchMetadata && typeof patchMetadata === "object") {
670
+ result.metadata = {
671
+ ...result.metadata ?? {},
672
+ ...patchMetadata
673
+ };
674
+ }
675
+ }
676
+ return result;
677
+ }
637
678
  function readStoredEmail() {
638
679
  if (typeof window === "undefined") return "";
639
680
  try {
@@ -658,7 +699,8 @@ function initUseroFeedbackWidget(props) {
658
699
  close: () => {
659
700
  },
660
701
  update: () => {
661
- }
702
+ },
703
+ whenReady: () => Promise.resolve()
662
704
  };
663
705
  }
664
706
  const { clientId, baseUrl } = props;
@@ -673,7 +715,8 @@ function initUseroFeedbackWidget(props) {
673
715
  close: () => {
674
716
  },
675
717
  update: () => {
676
- }
718
+ },
719
+ whenReady: () => Promise.resolve()
677
720
  };
678
721
  }
679
722
  let position = props.position ?? "right";
@@ -690,6 +733,34 @@ function initUseroFeedbackWidget(props) {
690
733
  let onOpen = props.onOpen;
691
734
  let onClose = props.onClose;
692
735
  const apiClient = new FeedbackApiClient(baseUrl);
736
+ const pluginList = props.plugins ?? [];
737
+ const pluginStores = /* @__PURE__ */ new Map();
738
+ const pluginContexts = /* @__PURE__ */ new Map();
739
+ const initPromises = [];
740
+ for (const plugin of pluginList) {
741
+ const ctx = {
742
+ clientId,
743
+ baseUrl: baseUrl ?? DEFAULT_API_URL,
744
+ logger: createPluginLogger(plugin.name),
745
+ getStore: () => pluginStores.get(plugin.name),
746
+ setStore: (value) => {
747
+ pluginStores.set(plugin.name, value);
748
+ }
749
+ };
750
+ pluginContexts.set(plugin.name, ctx);
751
+ if (plugin.onInit) {
752
+ const settled = (async () => {
753
+ try {
754
+ await plugin.onInit?.(ctx);
755
+ } catch (err) {
756
+ ctx.logger.error("onInit threw", err);
757
+ }
758
+ })();
759
+ initPromises.push(settled);
760
+ }
761
+ }
762
+ const readyPromise = initPromises.length === 0 ? Promise.resolve() : Promise.all(initPromises).then(() => {
763
+ });
693
764
  let isOpen = false;
694
765
  let selectedRating = void 0;
695
766
  let comment = "";
@@ -808,8 +879,24 @@ function initUseroFeedbackWidget(props) {
808
879
  setSubmitMessage({ type: "error", text: validation.errors.join(", ") });
809
880
  return;
810
881
  }
882
+ let enrichedSubmission = submission;
883
+ if (pluginList.length > 0) {
884
+ const patchPromises = pluginList.map(async (plugin) => {
885
+ if (!plugin.onFeedbackSubmit) return void 0;
886
+ const ctx = pluginContexts.get(plugin.name);
887
+ if (!ctx) return void 0;
888
+ try {
889
+ return await plugin.onFeedbackSubmit(ctx, submission);
890
+ } catch (err) {
891
+ ctx.logger.error("onFeedbackSubmit threw", err);
892
+ return void 0;
893
+ }
894
+ });
895
+ const patches = await Promise.all(patchPromises);
896
+ enrichedSubmission = mergePluginPatches(submission, patches);
897
+ }
811
898
  try {
812
- const response = await apiClient.submitFeedback(submission);
899
+ const response = await apiClient.submitFeedback(enrichedSubmission);
813
900
  if (response.success) {
814
901
  if (shareEmail && userEmail) writeStoredEmail(userEmail);
815
902
  onSubmit?.(feedbackData);
@@ -866,17 +953,26 @@ function initUseroFeedbackWidget(props) {
866
953
  const cls = ["fb-ec", sel && "fb-ec--sel"].filter(Boolean).join(" ");
867
954
  return `
868
955
  <div class="${cls}" style="background:${bg}">
869
- <button type="button" class="fb-eb" data-rating="${r}" role="radio" aria-checked="${sel}" aria-label="${r}: ${RATING_LABELS[r]}">
956
+ <button type="button" class="fb-eb" data-rating="${r}" role="radio" aria-checked="${sel}" aria-label="${r}: ${RATING_LABELS[r]}" style="color:${theme.text}">
870
957
  <div class="fb-ei"><span role="img" aria-label="${RATING_LABELS[r]}">${EMOJI_MAP[r]}</span></div>
871
- <div class="fb-el">${RATING_LABELS[r]}</div>
958
+ <div class="fb-el" style="color:${theme.text}">${RATING_LABELS[r]}</div>
872
959
  </button>
873
960
  </div>
874
961
  `;
875
962
  }).join("");
876
963
  const messageHtml = submitMessage ? `<div class="fb-msg fb-msg--header ${submitMessage.type === "success" ? "fb-msg--ok" : "fb-msg--err"}">${submitMessage.type === "success" ? "\u2713" : "\u26A0"} ${escapeHtml(submitMessage.text)}</div>` : "";
877
- const screenshotBlockHtml = showScreenshotOption ? (() => {
964
+ const uploadBtnHtml = showScreenshotOption ? (() => {
878
965
  const atMax = screenshots.length >= MAX_SCREENSHOTS;
879
966
  const btnDisabled = isUploadingScreenshot || atMax;
967
+ return `
968
+ <input type="file" accept="image/*" data-role="screenshot-input" style="display:none;" aria-label="Choose screenshot" />
969
+ <button type="button" class="fb-upb ${btnDisabled ? "fb-upb--dis" : ""}" data-role="screenshot-pick" ${btnDisabled ? "disabled" : ""} style="border:1px solid ${theme.border};color:${theme.text};">
970
+ ${isUploadingScreenshot ? '<span class="fb-ups"></span> Uploading...' : "\u{1F4F7} Add screenshot"}
971
+ </button>
972
+ `;
973
+ })() : "";
974
+ const uploadExtrasHtml = showScreenshotOption ? (() => {
975
+ const atMax = screenshots.length >= MAX_SCREENSHOTS;
880
976
  const previewsHtml = screenshots.map(
881
977
  (shot, i) => `
882
978
  <div class="fb-sp">
@@ -887,16 +983,7 @@ function initUseroFeedbackWidget(props) {
887
983
  ).join("");
888
984
  const errorHtml = screenshotError ? `<div class="fb-upe">\u26A0 ${escapeHtml(screenshotError)}</div>` : "";
889
985
  const limitHtml = atMax ? `<div class="fb-sl">Max ${MAX_SCREENSHOTS}</div>` : "";
890
- const extrasHtml = screenshotError || screenshots.length > 0 || atMax ? `<div class="fb-up-extras">${errorHtml}${screenshots.length > 0 ? `<div class="fb-ss">${previewsHtml}</div>` : ""}${limitHtml}</div>` : "";
891
- return `
892
- <div class="fb-up">
893
- <input type="file" accept="image/*" data-role="screenshot-input" style="display:none;" aria-label="Choose screenshot" />
894
- <button type="button" class="fb-upb ${btnDisabled ? "fb-upb--dis" : ""}" data-role="screenshot-pick" ${btnDisabled ? "disabled" : ""} style="border:1px solid ${theme.border};color:${theme.text};">
895
- ${isUploadingScreenshot ? '<span class="fb-ups"></span> Uploading...' : "\u{1F4F7} Add screenshot"}
896
- </button>
897
- ${extrasHtml}
898
- </div>
899
- `;
986
+ return screenshotError || screenshots.length > 0 || atMax ? `<div class="fb-up-extras">${errorHtml}${screenshots.length > 0 ? `<div class="fb-ss">${previewsHtml}</div>` : ""}${limitHtml}</div>` : "";
900
987
  })() : "";
901
988
  const emailBlockHtml = showEmailOption ? `
902
989
  <div class="fb-email">
@@ -919,8 +1006,11 @@ function initUseroFeedbackWidget(props) {
919
1006
  <form data-role="form">
920
1007
  <div class="fb-es" role="radiogroup" aria-label="Rate experience">${ratingsHtml}</div>
921
1008
  <textarea class="fb-ta" data-role="comment" placeholder="${escapeHtml(placeholder)}" aria-label="Comments" maxlength="1000" rows="2" style="border:1px solid ${theme.border};color:${theme.text};background-color:${theme.background};">${escapeHtml(comment)}</textarea>
922
- <div class="fb-charcount${lowChars ? " fb-charcount--low" : ""}" data-role="charcount" style="color:${lowChars ? "#dc2626" : theme.text};opacity:${lowChars ? 1 : 0.6};">${remaining} chars remaining</div>
923
- ${screenshotBlockHtml}
1009
+ <div class="fb-toolrow">
1010
+ ${uploadBtnHtml}
1011
+ <div class="fb-charcount${lowChars ? " fb-charcount--low" : ""}" data-role="charcount" style="color:${lowChars ? "#dc2626" : theme.text};opacity:${lowChars ? 1 : 0.6};">${remaining} chars remaining</div>
1012
+ </div>
1013
+ ${uploadExtrasHtml ? `<div class="fb-up">${uploadExtrasHtml}</div>` : ""}
924
1014
  ${emailBlockHtml}
925
1015
  <button class="fb-sub ${submitDisabled ? "fb-sub--dis" : ""}" type="submit" aria-label="Submit" ${submitDisabled ? "disabled" : ""} style="${submitStyle}">
926
1016
  ${isSubmitting ? '<span class="fb-spin"></span>' : ""}
@@ -1051,10 +1141,23 @@ function initUseroFeedbackWidget(props) {
1051
1141
  destroyed = true;
1052
1142
  document.removeEventListener("keydown", onKeyDown);
1053
1143
  detachMqlListener();
1144
+ for (const plugin of pluginList) {
1145
+ if (!plugin.onDestroy) continue;
1146
+ const ctx = pluginContexts.get(plugin.name);
1147
+ if (!ctx) continue;
1148
+ try {
1149
+ plugin.onDestroy(ctx);
1150
+ } catch (err) {
1151
+ ctx.logger.error("onDestroy threw", err);
1152
+ }
1153
+ }
1154
+ pluginStores.clear();
1155
+ pluginContexts.clear();
1054
1156
  host.remove();
1055
1157
  },
1056
1158
  open,
1057
1159
  close,
1160
+ whenReady: () => readyPromise,
1058
1161
  update: (next) => {
1059
1162
  if (destroyed) return;
1060
1163
  let needsRender = false;
@@ -1096,6 +1199,6 @@ function initUseroFeedbackWidget(props) {
1096
1199
  };
1097
1200
  }
1098
1201
 
1099
- export { DARK_THEME, DEFAULT_THEME, initUseroFeedbackWidget, mergeTheme, resolveTheme };
1202
+ export { DARK_THEME, DEFAULT_THEME, initUseroFeedbackWidget, mergePluginPatches, mergeTheme, resolveTheme };
1100
1203
  //# sourceMappingURL=vanilla.js.map
1101
1204
  //# sourceMappingURL=vanilla.js.map