feedbackkit-web 1.0.37 → 1.0.38

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.
@@ -307,9 +307,9 @@ class LogBuffer {
307
307
  this.entries = [];
308
308
  }
309
309
  push(level, message) {
310
- let text = redactMessage(message);
311
- if (text.length > MAX_MESSAGE_LENGTH) text = `${text.slice(0, MAX_MESSAGE_LENGTH)}… [truncated]`;
312
- this.entries.push({ level, message: text, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
310
+ let text2 = redactMessage(message);
311
+ if (text2.length > MAX_MESSAGE_LENGTH) text2 = `${text2.slice(0, MAX_MESSAGE_LENGTH)}… [truncated]`;
312
+ this.entries.push({ level, message: text2, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
313
313
  if (this.entries.length > this.maxEntries) this.entries.splice(0, this.entries.length - this.maxEntries);
314
314
  }
315
315
  snapshot() {
@@ -433,7 +433,7 @@ async function blobToBase64(blob) {
433
433
  }
434
434
  return btoa(binary);
435
435
  }
436
- async function encodePayload(report, projectKey) {
436
+ async function encodePayload(report, projectKey, identity = {}) {
437
437
  const payload = {
438
438
  project_key: projectKey,
439
439
  id: report.id,
@@ -465,13 +465,15 @@ async function encodePayload(report, projectKey) {
465
465
  }));
466
466
  }
467
467
  if (report.logs.length > 0) payload.logs = report.logs;
468
+ if (identity.reporterId) payload.reporter_id = identity.reporterId;
469
+ if (identity.user) payload.reporter = identity.user;
468
470
  return payload;
469
471
  }
470
- async function submitReport(report, configuration2) {
472
+ async function submitReport(report, configuration2, identity = {}) {
471
473
  if (!configuration2.projectKey) {
472
474
  throw new FeedbackSubmissionError("FeedbackKit is not configured with a project key.", "not_configured");
473
475
  }
474
- const body = JSON.stringify(await encodePayload(report, configuration2.projectKey));
476
+ const body = JSON.stringify(await encodePayload(report, configuration2.projectKey, identity));
475
477
  let response;
476
478
  try {
477
479
  response = await fetch(configuration2.endpoint ?? DEFAULT_ENDPOINT, {
@@ -510,6 +512,246 @@ async function fetchProducts(configuration2) {
510
512
  isDefault: p.is_default ?? false
511
513
  }));
512
514
  }
515
+ function compareBuilds(a, b) {
516
+ if (a == null || b == null || a === "" || b === "") return null;
517
+ if (a === b) return 0;
518
+ const numeric = /^[0-9]+(\.[0-9]+)*$/;
519
+ if (!numeric.test(a) || !numeric.test(b)) return null;
520
+ const pa = a.split(".");
521
+ const pb = b.split(".");
522
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
523
+ const na = BigInt(pa[i] ?? "0");
524
+ const nb = BigInt(pb[i] ?? "0");
525
+ if (na < nb) return -1;
526
+ if (na > nb) return 1;
527
+ }
528
+ return 0;
529
+ }
530
+ const STORAGE_KEY = "feedbackkit.reporterId";
531
+ const VALID_ID = /^[A-Za-z0-9_-]{16,128}$/;
532
+ let memoryId = null;
533
+ let currentUser = null;
534
+ function freshId() {
535
+ const bytes = new Uint8Array(16);
536
+ crypto.getRandomValues(bytes);
537
+ return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
538
+ }
539
+ function reporterId() {
540
+ if (memoryId) return memoryId;
541
+ try {
542
+ const stored = localStorage.getItem(STORAGE_KEY);
543
+ if (stored && VALID_ID.test(stored)) return memoryId = stored;
544
+ const id = freshId();
545
+ localStorage.setItem(STORAGE_KEY, id);
546
+ return memoryId = id;
547
+ } catch {
548
+ return memoryId = freshId();
549
+ }
550
+ }
551
+ function setUser(user) {
552
+ currentUser = user && (user.id || user.email || user.name) ? { ...user } : null;
553
+ }
554
+ function getUser() {
555
+ return currentUser;
556
+ }
557
+ function reporterUpdatesEndpoint(configuration2) {
558
+ if (configuration2.reporterUpdatesEndpoint) return configuration2.reporterUpdatesEndpoint;
559
+ const url = new URL(configuration2.endpoint ?? DEFAULT_ENDPOINT);
560
+ url.pathname = url.pathname.replace(/\/[^/]*\/?$/, "/reporter-updates");
561
+ url.search = "";
562
+ return url.toString();
563
+ }
564
+ function decodeUpdate(w) {
565
+ return {
566
+ feedbackId: w.feedback_id,
567
+ text: w.text ?? "",
568
+ createdAt: w.created_at ?? "",
569
+ screenName: w.screen_name ?? null,
570
+ fixStage: w.fix_stage ?? null,
571
+ fixedInBuild: w.fixed_in_build ?? null,
572
+ fixSummary: w.fix_summary ?? null,
573
+ needsVerification: w.needs_verification ?? false,
574
+ openQuestion: w.open_question ? { id: w.open_question.id, body: w.open_question.body, createdAt: w.open_question.created_at } : null,
575
+ screenshotUrl: w.screenshot_url ?? null,
576
+ messages: (w.messages ?? []).map((m) => ({ id: m.id, kind: m.kind, body: m.body, author: m.author, createdAt: m.created_at }))
577
+ };
578
+ }
579
+ async function fetchFixUpdates(configuration2) {
580
+ const url = new URL(reporterUpdatesEndpoint(configuration2));
581
+ url.searchParams.set("project_key", configuration2.projectKey);
582
+ url.searchParams.set("reporter_id", reporterId());
583
+ if (configuration2.appBuild) url.searchParams.set("build", configuration2.appBuild);
584
+ let response;
585
+ try {
586
+ response = await fetch(url.toString());
587
+ } catch (error) {
588
+ throw new FeedbackSubmissionError(`Network error: ${error instanceof Error ? error.message : String(error)}`, "network");
589
+ }
590
+ if (!response.ok) throw new FeedbackSubmissionError(`HTTP ${response.status}`, "server", response.status);
591
+ const json = await response.json();
592
+ return (json.updates ?? []).map(decodeUpdate);
593
+ }
594
+ async function encodeFixAction(configuration2, feedbackId, action) {
595
+ const body = {
596
+ project_key: configuration2.projectKey,
597
+ reporter_id: reporterId(),
598
+ feedback_id: feedbackId,
599
+ action: action.type
600
+ };
601
+ if (configuration2.appBuild) body.build = configuration2.appBuild;
602
+ if (action.type === "reply") body.text = action.text;
603
+ if (action.type === "reopen") {
604
+ const report = action.report;
605
+ const text2 = report?.text ?? action.text;
606
+ if (text2) body.text = text2;
607
+ if (report?.screenshotAnnotated) {
608
+ body.screenshot_annotated_png_base64 = await blobToBase64(report.screenshotAnnotated);
609
+ if (report.screenshotRaw) body.screenshot_raw_png_base64 = await blobToBase64(report.screenshotRaw);
610
+ body.annotations = report.annotations;
611
+ }
612
+ }
613
+ return body;
614
+ }
615
+ async function sendFixAction(configuration2, feedbackId, action) {
616
+ const body = JSON.stringify(await encodeFixAction(configuration2, feedbackId, action));
617
+ let response;
618
+ try {
619
+ response = await fetch(reporterUpdatesEndpoint(configuration2), {
620
+ method: "POST",
621
+ headers: { "Content-Type": "application/json" },
622
+ body
623
+ });
624
+ } catch (error) {
625
+ throw new FeedbackSubmissionError(`Network error: ${error instanceof Error ? error.message : String(error)}`, "network");
626
+ }
627
+ if (!response.ok) throw new FeedbackSubmissionError(`HTTP ${response.status}`, "server", response.status);
628
+ }
629
+ const svg = (body) => `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
630
+ const icons = {
631
+ feedback: svg(
632
+ '<path d="M21 12a8 8 0 0 1-11.6 7.1L4 20l1-4.6A8 8 0 1 1 21 12Z"/><path d="M8.5 10.5h7M8.5 14h4.5"/>'
633
+ ),
634
+ pen: svg('<path d="M4 20l4-1L19 8a2.1 2.1 0 0 0-3-3L5 16l-1 4Z"/><path d="M14.5 6.5l3 3"/>'),
635
+ rectangle: svg('<rect x="4" y="6" width="16" height="12" rx="1.5"/>'),
636
+ arrow: svg('<path d="M6 18L18 6"/><path d="M9 6h9v9"/>'),
637
+ text: svg('<path d="M5 6V5h14v1"/><path d="M12 5v14"/><path d="M9 19h6"/>'),
638
+ move: svg(
639
+ '<path d="M12 3v18M3 12h18"/><path d="M9 6l3-3 3 3M9 18l3 3 3-3M6 9l-3 3 3 3M18 9l3 3-3 3"/>'
640
+ ),
641
+ undo: svg('<path d="M9 14L4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>'),
642
+ trash: svg('<path d="M4 7h16"/><path d="M10 11v6M14 11v6"/><path d="M6 7l1 13h10l1-13"/><path d="M9 7V4h6v3"/>'),
643
+ close: svg('<path d="M6 6l12 12M18 6L6 18"/>'),
644
+ paperclip: svg(
645
+ '<path d="M21 11.5l-8.6 8.6a5.5 5.5 0 0 1-7.8-7.8l8.6-8.6a3.7 3.7 0 0 1 5.2 5.2l-8.6 8.6a1.8 1.8 0 0 1-2.6-2.6l7.9-7.9"/>'
646
+ ),
647
+ send: svg('<path d="M4 12l16-8-6 16-3-7-7-1Z"/>'),
648
+ check: svg('<path d="M5 12.5l4.5 4.5L19 7.5"/>')
649
+ };
650
+ class FixCard {
651
+ constructor(shadow, update, onOutcome) {
652
+ this.shadow = shadow;
653
+ this.update = update;
654
+ this.onOutcome = onOutcome;
655
+ this.root = null;
656
+ this.settled = false;
657
+ }
658
+ show(position) {
659
+ const u = this.update;
660
+ const card = el$1("section", "fk-fixcard");
661
+ card.dataset.position = position;
662
+ card.setAttribute("role", "dialog");
663
+ card.setAttribute("aria-labelledby", "fk-fixcard-title");
664
+ const header = el$1("div", "fk-fixcard-header");
665
+ const title = el$1("h2", "fk-title");
666
+ title.id = "fk-fixcard-title";
667
+ title.textContent = u.needsVerification ? "We fixed something you reported" : "A question about your report";
668
+ const close = document.createElement("button");
669
+ close.type = "button";
670
+ close.className = "fk-close";
671
+ close.innerHTML = icons.close;
672
+ close.setAttribute("aria-label", "Remind me later");
673
+ close.title = "Remind me later";
674
+ close.addEventListener("click", () => this.settle({ type: "later" }));
675
+ header.append(title, close);
676
+ card.append(header);
677
+ if (u.needsVerification && u.fixedInBuild) {
678
+ card.append(text("p", "fk-meta", `Fixed in build ${u.fixedInBuild}.`));
679
+ }
680
+ if (u.screenshotUrl) {
681
+ const img = document.createElement("img");
682
+ img.className = "fk-fixcard-shot";
683
+ img.alt = "Your screenshot from the original report";
684
+ img.src = u.screenshotUrl;
685
+ img.addEventListener("error", () => img.remove());
686
+ card.append(img);
687
+ }
688
+ card.append(text("p", "fk-meta", "You reported"));
689
+ card.append(text("p", "fk-fixcard-quote", u.text ? `“${u.text}”` : "(no description)"));
690
+ if (u.needsVerification && u.fixSummary) {
691
+ card.append(text("p", "fk-fixcard-summary", `What changed: ${u.fixSummary}`));
692
+ }
693
+ for (const m of u.messages.filter((m2) => m2.kind === "comment")) {
694
+ const msg = el$1("div", "fk-fixcard-message");
695
+ msg.append(text("span", "fk-meta", m.author), text("p", "", m.body));
696
+ card.append(msg);
697
+ }
698
+ const actions = el$1("div", "fk-fixcard-actions");
699
+ if (u.needsVerification) {
700
+ const yes = button("fk-btn fk-btn-primary", "Yes, it's fixed");
701
+ yes.addEventListener("click", () => this.settle({ type: "verified" }));
702
+ const no = button("fk-btn fk-btn-secondary", "No, still broken");
703
+ no.addEventListener("click", () => this.settle({ type: "stillBroken" }));
704
+ actions.append(yes, no);
705
+ } else if (u.openQuestion) {
706
+ actions.append(text("p", "fk-fixcard-question", u.openQuestion.body));
707
+ const input = document.createElement("textarea");
708
+ input.className = "fk-textarea";
709
+ input.placeholder = "Your answer";
710
+ input.setAttribute("aria-label", "Your answer");
711
+ const send = button("fk-btn fk-btn-primary", "Send");
712
+ send.disabled = true;
713
+ input.addEventListener("input", () => send.disabled = input.value.trim() === "");
714
+ send.addEventListener("click", () => this.settle({ type: "replied", text: input.value.trim() }));
715
+ actions.append(input, send);
716
+ }
717
+ card.append(actions);
718
+ this.root = card;
719
+ this.shadow.append(card);
720
+ }
721
+ /** Close as "later" (e.g. `FeedbackKit.disableFixVerification()`). */
722
+ dismiss() {
723
+ this.settle({ type: "later" });
724
+ }
725
+ /** Remove without reporting an outcome (the widget is being destroyed). */
726
+ remove() {
727
+ this.settled = true;
728
+ this.root?.remove();
729
+ this.root = null;
730
+ }
731
+ settle(outcome) {
732
+ if (this.settled) return;
733
+ this.settled = true;
734
+ this.root?.remove();
735
+ this.root = null;
736
+ this.onOutcome(outcome);
737
+ }
738
+ }
739
+ function el$1(tag, className) {
740
+ const node = document.createElement(tag);
741
+ if (className) node.className = className;
742
+ return node;
743
+ }
744
+ function text(tag, className, content) {
745
+ const node = el$1(tag, className);
746
+ node.textContent = content;
747
+ return node;
748
+ }
749
+ function button(className, label) {
750
+ const b = el$1("button", className);
751
+ b.type = "button";
752
+ b.textContent = label;
753
+ return b;
754
+ }
513
755
  const STROKE_WIDTH = 4;
514
756
  const HIT_TEST_TOLERANCE = 16;
515
757
  const HEAD_LENGTH = 18;
@@ -628,8 +870,8 @@ function arrowGeometry(rawStart, rawEnd, scale = 1, rotation = 0) {
628
870
  };
629
871
  return { start, end, left, right };
630
872
  }
631
- function drawText(ctx, text, point, color, scale) {
632
- const rect = textBubbleRect(ctx, text, point, scale);
873
+ function drawText(ctx, text2, point, color, scale) {
874
+ const rect = textBubbleRect(ctx, text2, point, scale);
633
875
  ctx.save();
634
876
  ctx.fillStyle = color;
635
877
  ctx.beginPath();
@@ -638,7 +880,7 @@ function drawText(ctx, text, point, color, scale) {
638
880
  ctx.font = textFont(scale);
639
881
  ctx.fillStyle = "#FFFFFF";
640
882
  ctx.textBaseline = "top";
641
- ctx.fillText(text, rect.x + TEXT_PADDING, rect.y + TEXT_PADDING + BASE_FONT_SIZE * scale * (LINE_HEIGHT_RATIO - 1) / 2);
883
+ ctx.fillText(text2, rect.x + TEXT_PADDING, rect.y + TEXT_PADDING + BASE_FONT_SIZE * scale * (LINE_HEIGHT_RATIO - 1) / 2);
642
884
  ctx.restore();
643
885
  }
644
886
  function roundedRectPath(ctx, x, y, w, h, r) {
@@ -650,10 +892,10 @@ function roundedRectPath(ctx, x, y, w, h, r) {
650
892
  ctx.arcTo(x, y, x + w, y, radius);
651
893
  ctx.closePath();
652
894
  }
653
- function textBubbleRect(measurer, text, point, scale = 1) {
895
+ function textBubbleRect(measurer, text2, point, scale = 1) {
654
896
  const previous = measurer.font;
655
897
  measurer.font = textFont(scale);
656
- const width = measurer.measureText(text).width;
898
+ const width = measurer.measureText(text2).width;
657
899
  measurer.font = previous;
658
900
  const height = BASE_FONT_SIZE * scale * LINE_HEIGHT_RATIO;
659
901
  const padding = TEXT_PADDING * scale;
@@ -1055,27 +1297,6 @@ function clamp(v, lo, hi) {
1055
1297
  function round4(v) {
1056
1298
  return Math.round(v * 1e4) / 1e4;
1057
1299
  }
1058
- const svg = (body) => `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
1059
- const icons = {
1060
- feedback: svg(
1061
- '<path d="M21 12a8 8 0 0 1-11.6 7.1L4 20l1-4.6A8 8 0 1 1 21 12Z"/><path d="M8.5 10.5h7M8.5 14h4.5"/>'
1062
- ),
1063
- pen: svg('<path d="M4 20l4-1L19 8a2.1 2.1 0 0 0-3-3L5 16l-1 4Z"/><path d="M14.5 6.5l3 3"/>'),
1064
- rectangle: svg('<rect x="4" y="6" width="16" height="12" rx="1.5"/>'),
1065
- arrow: svg('<path d="M6 18L18 6"/><path d="M9 6h9v9"/>'),
1066
- text: svg('<path d="M5 6V5h14v1"/><path d="M12 5v14"/><path d="M9 19h6"/>'),
1067
- move: svg(
1068
- '<path d="M12 3v18M3 12h18"/><path d="M9 6l3-3 3 3M9 18l3 3 3-3M6 9l-3 3 3 3M18 9l3 3-3 3"/>'
1069
- ),
1070
- undo: svg('<path d="M9 14L4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>'),
1071
- trash: svg('<path d="M4 7h16"/><path d="M10 11v6M14 11v6"/><path d="M6 7l1 13h10l1-13"/><path d="M9 7V4h6v3"/>'),
1072
- close: svg('<path d="M6 6l12 12M18 6L6 18"/>'),
1073
- paperclip: svg(
1074
- '<path d="M21 11.5l-8.6 8.6a5.5 5.5 0 0 1-7.8-7.8l8.6-8.6a3.7 3.7 0 0 1 5.2 5.2l-8.6 8.6a1.8 1.8 0 0 1-2.6-2.6l7.9-7.9"/>'
1075
- ),
1076
- send: svg('<path d="M4 12l16-8-6 16-3-7-7-1Z"/>'),
1077
- check: svg('<path d="M5 12.5l4.5 4.5L19 7.5"/>')
1078
- };
1079
1300
  const styles = (
1080
1301
  /* css */
1081
1302
  `
@@ -1332,6 +1553,39 @@ button:disabled { cursor: not-allowed; opacity: 0.45; }
1332
1553
  .fk-done-icon svg { width: 26px; height: 26px; }
1333
1554
  .fk-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
1334
1555
 
1556
+ /* ---------- "is it fixed?" card (fixCard.ts) ---------- */
1557
+ .fk-fixcard {
1558
+ position: fixed;
1559
+ z-index: 2147483000;
1560
+ right: 20px;
1561
+ bottom: 76px;
1562
+ width: min(360px, calc(100vw - 32px));
1563
+ max-height: min(640px, calc(100vh - 96px));
1564
+ overflow: auto;
1565
+ display: flex;
1566
+ flex-direction: column;
1567
+ gap: 8px;
1568
+ padding: 16px;
1569
+ border: 1px solid var(--fk-border);
1570
+ border-radius: 16px;
1571
+ background: var(--fk-bg);
1572
+ box-shadow: var(--fk-shadow);
1573
+ animation: fk-rise 0.2s ease-out;
1574
+ }
1575
+ .fk-fixcard[data-position="bottom-left"] { right: auto; left: 20px; }
1576
+ .fk-fixcard > *, .fk-fixcard-actions > * { flex-shrink: 0; }
1577
+ .fk-fixcard p { margin: 0; }
1578
+ .fk-fixcard-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
1579
+ .fk-fixcard-shot { display: block; width: 100%; max-height: 220px; object-fit: contain; border-radius: 10px; background: var(--fk-surface); }
1580
+ .fk-fixcard-quote { font-size: 14px; }
1581
+ .fk-fixcard-summary { font-size: 13px; padding: 8px 10px; border-radius: 8px; background: var(--fk-surface); }
1582
+ .fk-fixcard-message p { font-size: 13px; }
1583
+ .fk-fixcard-question { font-weight: 600; }
1584
+ .fk-fixcard-actions { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
1585
+ .fk-fixcard-actions .fk-btn { flex: none; width: 100%; }
1586
+ .fk-fixcard-actions .fk-textarea { min-height: 64px; }
1587
+ @keyframes fk-rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
1588
+
1335
1589
  /* ---------- narrow screens: stack stage over composer ---------- */
1336
1590
  @media (max-width: 760px) {
1337
1591
  .fk-overlay { padding: 0; }
@@ -1341,9 +1595,10 @@ button:disabled { cursor: not-allowed; opacity: 0.45; }
1341
1595
  .fk-canvas-wrap { padding: 10px; }
1342
1596
  .fk-composer { max-height: 55vh; padding: 14px; gap: 10px; }
1343
1597
  .fk-textarea { min-height: 84px; }
1598
+ .fk-fixcard, .fk-fixcard[data-position] { right: 16px; left: 16px; width: auto; bottom: 16px; }
1344
1599
  }
1345
1600
  @media (prefers-reduced-motion: reduce) {
1346
- .fk-overlay, .fk-dialog { animation: none; }
1601
+ .fk-overlay, .fk-dialog, .fk-fixcard { animation: none; }
1347
1602
  }
1348
1603
  `
1349
1604
  );
@@ -1369,26 +1624,48 @@ class Widget {
1369
1624
  this.shadow = null;
1370
1625
  this.trigger = null;
1371
1626
  this.pending = null;
1627
+ this.fixCard = null;
1372
1628
  this.isOwnNode = (node) => node === this.host;
1373
1629
  }
1630
+ get isShowingFixCard() {
1631
+ return this.fixCard !== null;
1632
+ }
1633
+ /**
1634
+ * The non-modal "is it fixed?" card (bottom corner). Resolves with what
1635
+ * the reporter chose; the card removes itself.
1636
+ */
1637
+ showFixCard(update) {
1638
+ this.fixCard?.remove();
1639
+ const shadow = this.mount();
1640
+ return new Promise((resolve) => {
1641
+ this.fixCard = new FixCard(shadow, update, (outcome) => {
1642
+ this.fixCard = null;
1643
+ resolve(outcome);
1644
+ });
1645
+ this.fixCard.show(this.trigger?.dataset.position === "bottom-left" ? "bottom-left" : "bottom-right");
1646
+ });
1647
+ }
1648
+ hideFixCard() {
1649
+ this.fixCard?.dismiss();
1650
+ }
1374
1651
  get isOpen() {
1375
1652
  return this.pending !== null;
1376
1653
  }
1377
1654
  showTrigger(options, onClick) {
1378
1655
  const shadow = this.mount();
1379
1656
  this.trigger?.remove();
1380
- const button = document.createElement("button");
1381
- button.type = "button";
1382
- button.className = "fk-trigger";
1383
- button.dataset.position = options.position ?? "bottom-right";
1384
- button.dataset.compact = String(!!options.compact);
1657
+ const button2 = document.createElement("button");
1658
+ button2.type = "button";
1659
+ button2.className = "fk-trigger";
1660
+ button2.dataset.position = options.position ?? "bottom-right";
1661
+ button2.dataset.compact = String(!!options.compact);
1385
1662
  const label = options.label ?? "Feedback";
1386
- button.setAttribute("aria-label", label);
1387
- button.innerHTML = `${icons.feedback}<span class="fk-trigger-label"></span>`;
1388
- button.querySelector(".fk-trigger-label").textContent = label;
1389
- button.addEventListener("click", onClick);
1390
- shadow.append(button);
1391
- this.trigger = button;
1663
+ button2.setAttribute("aria-label", label);
1664
+ button2.innerHTML = `${icons.feedback}<span class="fk-trigger-label"></span>`;
1665
+ button2.querySelector(".fk-trigger-label").textContent = label;
1666
+ button2.addEventListener("click", onClick);
1667
+ shadow.append(button2);
1668
+ this.trigger = button2;
1392
1669
  this.applyTheme();
1393
1670
  }
1394
1671
  hideTrigger() {
@@ -1397,6 +1674,8 @@ class Widget {
1397
1674
  }
1398
1675
  destroy() {
1399
1676
  this.hideTrigger();
1677
+ this.fixCard?.remove();
1678
+ this.fixCard = null;
1400
1679
  this.host?.remove();
1401
1680
  this.host = null;
1402
1681
  this.shadow = null;
@@ -1580,17 +1859,17 @@ class Dialog {
1580
1859
  const header = el("div", "fk-header");
1581
1860
  const title = el("h2", "fk-title");
1582
1861
  title.id = "fk-title";
1583
- title.textContent = "Send feedback";
1862
+ title.textContent = this.options.copy?.title ?? "Send feedback";
1584
1863
  const close = iconButton("fk-close", icons.close, "Close");
1585
1864
  close.addEventListener("click", () => this.cancel());
1586
1865
  header.append(title, close);
1587
1866
  const textLabel = el("label", "fk-label");
1588
- textLabel.textContent = "What's the problem?";
1867
+ textLabel.textContent = this.options.copy?.label ?? "What's the problem?";
1589
1868
  textLabel.setAttribute("for", "fk-text");
1590
1869
  this.textarea = document.createElement("textarea");
1591
1870
  this.textarea.id = "fk-text";
1592
1871
  this.textarea.className = "fk-textarea";
1593
- this.textarea.placeholder = "Describe what happened and what you expected instead…";
1872
+ this.textarea.placeholder = this.options.copy?.placeholder ?? "Describe what happened and what you expected instead…";
1594
1873
  this.textarea.addEventListener("input", () => this.refreshState());
1595
1874
  const textGroup = el("div");
1596
1875
  textGroup.append(textLabel, this.textarea);
@@ -1816,14 +2095,16 @@ class Dialog {
1816
2095
  this.sendButton.innerHTML = `<span class="fk-spinner" aria-hidden="true"></span><span>Sending…</span>`;
1817
2096
  try {
1818
2097
  const report = await this.buildReport();
1819
- if (this.options.submit) {
2098
+ if (this.options.deliver) {
2099
+ await this.options.deliver(report);
2100
+ } else if (this.options.submit) {
1820
2101
  const configuration2 = this.deps.configuration();
1821
2102
  if (!configuration2) throw new Error("FeedbackKit.configure() hasn't been called.");
1822
- await submitReport(report, configuration2);
2103
+ await submitReport(report, configuration2, this.deps.identity());
1823
2104
  }
1824
2105
  await this.options.onReport?.(report);
1825
2106
  this.showThanks();
1826
- setTimeout(() => this.finish(report), this.options.submit ? 1300 : 0);
2107
+ setTimeout(() => this.finish(report), this.delivers ? 1300 : 0);
1827
2108
  } catch (error) {
1828
2109
  this.sending = false;
1829
2110
  this.sendButton.innerHTML = `${icons.send}<span>Retry</span>`;
@@ -1832,8 +2113,11 @@ class Dialog {
1832
2113
  this.options.onError?.(error);
1833
2114
  }
1834
2115
  }
2116
+ get delivers() {
2117
+ return this.options.submit || !!this.options.deliver;
2118
+ }
1835
2119
  showThanks() {
1836
- if (!this.options.submit) return;
2120
+ if (!this.delivers) return;
1837
2121
  const composer = this.overlay.querySelector(".fk-composer");
1838
2122
  if (!composer) return;
1839
2123
  const done = el("div", "fk-done");
@@ -1841,9 +2125,9 @@ class Dialog {
1841
2125
  const icon = el("span", "fk-done-icon");
1842
2126
  icon.innerHTML = icons.check;
1843
2127
  const title = el("h2", "fk-title");
1844
- title.textContent = "Thanks for the feedback!";
2128
+ title.textContent = this.options.copy?.thanksTitle ?? "Thanks for the feedback!";
1845
2129
  const sub = el("p", "fk-meta");
1846
- sub.textContent = "It's on its way to the team.";
2130
+ sub.textContent = this.options.copy?.thanksBody ?? "It's on its way to the team.";
1847
2131
  done.append(icon, title, sub);
1848
2132
  composer.replaceChildren(done);
1849
2133
  }
@@ -1884,9 +2168,9 @@ function switchControl(label, checked, onChange) {
1884
2168
  input.setAttribute("role", "switch");
1885
2169
  input.addEventListener("change", () => onChange(input.checked));
1886
2170
  const track = el("span", "fk-switch-track");
1887
- const text = el("span");
1888
- text.textContent = label;
1889
- wrap.append(input, track, text);
2171
+ const text2 = el("span");
2172
+ text2.textContent = label;
2173
+ wrap.append(input, track, text2);
1890
2174
  return wrap;
1891
2175
  }
1892
2176
  function validHex(hex) {
@@ -1915,8 +2199,66 @@ const widget = new Widget({
1915
2199
  defaultProductKey: () => configuration?.defaultProductKey,
1916
2200
  logs: () => logCapture?.buffer.snapshot() ?? [],
1917
2201
  captureOptions: () => FeedbackKit.captureOptions,
1918
- currentScreen: () => FeedbackKit.currentScreen
2202
+ currentScreen: () => FeedbackKit.currentScreen,
2203
+ identity: () => ({ reporterId: reporterId(), user: getUser() })
1919
2204
  });
2205
+ const FIX_CHECK_INTERVAL_MS = 6e4;
2206
+ let fixVerificationEnabled = false;
2207
+ let lastFixCheck = 0;
2208
+ let fixCheckInFlight = false;
2209
+ const handledThisPage = /* @__PURE__ */ new Set();
2210
+ function onVisibilityChange() {
2211
+ if (document.visibilityState === "visible") void checkAndShowFix(false);
2212
+ }
2213
+ async function checkAndShowFix(force) {
2214
+ const config = configuration;
2215
+ if (!fixVerificationEnabled || !config || fixCheckInFlight || widget.isOpen || widget.isShowingFixCard) return;
2216
+ if (!force && Date.now() - lastFixCheck < FIX_CHECK_INTERVAL_MS) return;
2217
+ fixCheckInFlight = true;
2218
+ lastFixCheck = Date.now();
2219
+ let next;
2220
+ try {
2221
+ next = (await fetchFixUpdates(config)).find((u) => !handledThisPage.has(u.feedbackId));
2222
+ } catch {
2223
+ return;
2224
+ } finally {
2225
+ fixCheckInFlight = false;
2226
+ }
2227
+ if (!next || !fixVerificationEnabled) return;
2228
+ void handleFixCard(config, next);
2229
+ }
2230
+ async function handleFixCard(config, update) {
2231
+ const outcome = await widget.showFixCard(update);
2232
+ handledThisPage.add(update.feedbackId);
2233
+ const send = (action) => sendFixAction(config, update.feedbackId, action).catch(
2234
+ (error) => console.warn("[FeedbackKit] Couldn't send fix verification.", error)
2235
+ );
2236
+ switch (outcome.type) {
2237
+ case "verified":
2238
+ await send({ type: "verify" });
2239
+ void checkAndShowFix(true);
2240
+ break;
2241
+ case "replied":
2242
+ await send({ type: "reply", text: outcome.text });
2243
+ void checkAndShowFix(true);
2244
+ break;
2245
+ case "stillBroken": {
2246
+ const report = await widget.open({
2247
+ submit: false,
2248
+ deliver: (r) => sendFixAction(config, update.feedbackId, { type: "reopen", report: r }),
2249
+ copy: {
2250
+ title: "Still broken",
2251
+ label: "What's still wrong?",
2252
+ placeholder: "Show us on the screenshot and describe what you still see…",
2253
+ thanksTitle: "Thanks — we're on it",
2254
+ thanksBody: "Your report was reopened and sent back to the team."
2255
+ }
2256
+ });
2257
+ if (!report) await send({ type: "reopen" });
2258
+ break;
2259
+ }
2260
+ }
2261
+ }
1920
2262
  function resolveProducts() {
1921
2263
  if (products.length > 0 || !configuration) return Promise.resolve(products);
1922
2264
  if (!productsPromise) {
@@ -1995,7 +2337,7 @@ const FeedbackKit = {
1995
2337
  if (!configuration) {
1996
2338
  return Promise.reject(new Error("FeedbackKit.configure({ projectKey }) must be called before submit()."));
1997
2339
  }
1998
- return submitReport(report, configuration);
2340
+ return submitReport(report, configuration, { reporterId: reporterId(), user: getUser() });
1999
2341
  },
2000
2342
  get isPresenting() {
2001
2343
  return widget.isOpen;
@@ -2049,8 +2391,69 @@ const FeedbackKit = {
2049
2391
  const shot = await captureViewport({ ...FeedbackKit.captureOptions, ...options, exclude: widget.isOwnNode });
2050
2392
  return canvasToPngBlob(shot.canvas);
2051
2393
  },
2394
+ /**
2395
+ * Optional identity of the person using your site, attached to every
2396
+ * report they submit. Not needed for fix verification (that uses an
2397
+ * anonymous per-browser id, `reporterId`). Pass `null` on sign-out.
2398
+ */
2399
+ setUser(user) {
2400
+ setUser(user);
2401
+ },
2402
+ /** The anonymous per-browser id attached to reports — how a fix finds its way back here. */
2403
+ get reporterId() {
2404
+ return reporterId();
2405
+ },
2406
+ /**
2407
+ * This browser's reports that need the reporter's attention: shipped
2408
+ * fixes to confirm and developer/agent questions. For a custom UI — the
2409
+ * built-in one is `enableFixVerification`.
2410
+ */
2411
+ checkForFixUpdates() {
2412
+ if (!configuration) {
2413
+ return Promise.reject(new Error("FeedbackKit.configure({ projectKey }) must be called before checkForFixUpdates()."));
2414
+ }
2415
+ return fetchFixUpdates(configuration);
2416
+ },
2417
+ /** Answers one update directly (custom UIs): verify, reopen, or reply. */
2418
+ respondToFixUpdate(feedbackId, action) {
2419
+ if (!configuration) {
2420
+ return Promise.reject(new Error("FeedbackKit.configure({ projectKey }) must be called before respondToFixUpdate()."));
2421
+ }
2422
+ return sendFixAction(configuration, feedbackId, action);
2423
+ },
2424
+ /**
2425
+ * Closes the loop with the person who reported a bug: once a fix for one
2426
+ * of their reports ships (announced with `feedbackkit release`), a small
2427
+ * card shows their original screenshot and asks "is it fixed?". "Still
2428
+ * broken" reopens the capture dialog so they can show what's wrong now,
2429
+ * and the report goes straight back to the developer. Also surfaces
2430
+ * questions the developer or their coding agent asked.
2431
+ *
2432
+ * Checks now and whenever the tab becomes visible (at most once a minute).
2433
+ * With `appBuild` configured, only fixes shipped in that build or earlier
2434
+ * are shown; without it, a fix counts as live as soon as it's released.
2435
+ */
2436
+ enableFixVerification() {
2437
+ if (fixVerificationEnabled) return;
2438
+ fixVerificationEnabled = true;
2439
+ document.addEventListener("visibilitychange", onVisibilityChange);
2440
+ whenBodyReady(() => void checkAndShowFix(true));
2441
+ },
2442
+ disableFixVerification() {
2443
+ fixVerificationEnabled = false;
2444
+ document.removeEventListener("visibilitychange", onVisibilityChange);
2445
+ widget.hideFixCard();
2446
+ },
2447
+ /**
2448
+ * Checks right now (ignoring the throttle) and shows the card if there's
2449
+ * something. Resolves once the check is done and any card is on screen.
2450
+ */
2451
+ presentFixUpdatesIfNeeded() {
2452
+ return checkAndShowFix(true);
2453
+ },
2052
2454
  /** Removes every trace of the SDK from the page (trigger, dialog host, listeners, log hooks). */
2053
2455
  destroy() {
2456
+ FeedbackKit.disableFixVerification();
2054
2457
  FeedbackKit.disableKeyboardShortcut();
2055
2458
  logCapture?.uninstall();
2056
2459
  logCapture = null;
@@ -2062,6 +2465,7 @@ export {
2062
2465
  FeedbackKit,
2063
2466
  FeedbackSubmissionError,
2064
2467
  STROKE_WIDTH,
2468
+ compareBuilds,
2065
2469
  FeedbackKit as default,
2066
2470
  drawAnnotation,
2067
2471
  drawAnnotations,
@@ -2070,6 +2474,7 @@ export {
2070
2474
  parseUserAgent,
2071
2475
  readPoint,
2072
2476
  redactUrl,
2477
+ reporterUpdatesEndpoint,
2073
2478
  textBubbleRect
2074
2479
  };
2075
2480
  //# sourceMappingURL=feedbackkit.js.map