@reqdesk/widget 0.1.0 → 0.3.0

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/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as submitTrackingReply, a as saveTrackingToken, b as configureWidgetClient, c as getWidgetStyles, d as en, f as getTicketDetail, g as submitTicket, h as submitReply, i as loadWidgetEmail, l as themeToVars, m as resolveWidgetUser, n as getTrackingTokens, o as saveWidgetConfig, p as listMyTickets, r as loadWidgetConfig, s as saveWidgetEmail, t as clearWidgetEmail, u as ar, v as trackTicket, x as setOidcTokenProvider, y as uploadAttachment } from "./storage-CC5BCsxP.js";
1
+ import { C as configureWidgetClient, S as uploadAttachment, _ as resolveWidgetUser, a as saveTrackingToken, b as submitTrackingReply, c as getWidgetStyles, d as ar, f as en, g as listMyTickets, h as getTicketDetail, i as loadWidgetEmail, l as themeToStyle, m as getCategories, n as getTrackingTokens, o as saveWidgetConfig, p as closeTicket, r as loadWidgetConfig, s as saveWidgetEmail, t as clearWidgetEmail, v as submitReply, w as setOidcTokenProvider, x as trackTicket, y as submitTicket } from "./storage-BG7rsgWE.js";
2
2
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { QueryClient, QueryClientProvider, keepPreviousData, queryOptions, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -31,7 +31,11 @@ function onAuthStateChange(listener) {
31
31
  listeners = listeners.filter((l) => l !== listener);
32
32
  };
33
33
  }
34
- async function initWidgetAuth(config) {
34
+ function initWidgetAuth(config) {
35
+ initPromise = _initWidgetAuth(config);
36
+ return initPromise;
37
+ }
38
+ async function _initWidgetAuth(config) {
35
39
  setState({ isLoading: true });
36
40
  try {
37
41
  const { createOidc } = await import("oidc-spa/core");
@@ -68,8 +72,13 @@ async function initWidgetAuth(config) {
68
72
  });
69
73
  }
70
74
  }
75
+ let initPromise = null;
71
76
  async function login() {
72
- if (!oidcInstance) return;
77
+ if (!oidcInstance && initPromise) await initPromise;
78
+ if (!oidcInstance) {
79
+ console.warn("[reqdesk-widget] Cannot login: OIDC not initialized. Check auth config.");
80
+ return;
81
+ }
73
82
  await oidcInstance.login({ doesCurrentHrefRequiresAuth: false });
74
83
  }
75
84
  async function logout() {
@@ -297,7 +306,7 @@ function TicketForm({ mode = "inline", onTicketCreated, onError, className, styl
297
306
  onError,
298
307
  t
299
308
  ]);
300
- const cssVars = themeToVars(ctx.theme);
309
+ const cssVars = themeToStyle(ctx.theme);
301
310
  const content = success ? /* @__PURE__ */ jsxs("div", {
302
311
  className: "rqd-success",
303
312
  style: {
@@ -435,7 +444,7 @@ function TicketForm({ mode = "inline", onTicketCreated, onError, className, styl
435
444
  className,
436
445
  style: {
437
446
  ...style,
438
- cssText: cssVars
447
+ ...cssVars
439
448
  },
440
449
  children: /* @__PURE__ */ jsx("div", {
441
450
  className: "rqd-body",
@@ -444,7 +453,7 @@ function TicketForm({ mode = "inline", onTicketCreated, onError, className, styl
444
453
  }) });
445
454
  return /* @__PURE__ */ jsx(ShadowRoot, { children: /* @__PURE__ */ jsx("div", {
446
455
  className: `rqd-inline ${className ?? ""}`,
447
- style: { cssText: cssVars },
456
+ style: cssVars,
448
457
  children: /* @__PURE__ */ jsx("div", {
449
458
  className: "rqd-body",
450
459
  children: content
@@ -456,10 +465,10 @@ function TicketForm({ mode = "inline", onTicketCreated, onError, className, styl
456
465
  function SupportPortal({ className }) {
457
466
  const ctx = useReqdeskContext();
458
467
  const { isLoading } = useReqdesk();
459
- const cssVars = themeToVars(ctx.theme);
468
+ const cssVars = themeToStyle(ctx.theme);
460
469
  return /* @__PURE__ */ jsx(ShadowRoot, { children: /* @__PURE__ */ jsx("div", {
461
470
  className: `rqd-inline ${className ?? ""}`,
462
- style: { cssText: cssVars },
471
+ style: cssVars,
463
472
  children: /* @__PURE__ */ jsx("div", {
464
473
  className: "rqd-body",
465
474
  children: isLoading ? /* @__PURE__ */ jsx("p", {
@@ -480,6 +489,132 @@ function SupportPortal({ className }) {
480
489
  }) });
481
490
  }
482
491
  //#endregion
492
+ //#region src/react/queries.ts
493
+ const widgetTicketDetailOptions = (ticketId) => queryOptions({
494
+ queryKey: ["widget-ticket", ticketId],
495
+ queryFn: () => getTicketDetail(ticketId),
496
+ staleTime: 6e4,
497
+ enabled: !!ticketId
498
+ });
499
+ const widgetMyTicketsOptions = (projectId, userId) => queryOptions({
500
+ queryKey: [
501
+ "widget-tickets",
502
+ projectId,
503
+ userId
504
+ ],
505
+ queryFn: () => listMyTickets(projectId, userId),
506
+ staleTime: 3e4,
507
+ placeholderData: keepPreviousData,
508
+ enabled: !!userId
509
+ });
510
+ const widgetUserOptions = (projectId, email) => queryOptions({
511
+ queryKey: [
512
+ "widget-user",
513
+ projectId,
514
+ email
515
+ ],
516
+ queryFn: () => resolveWidgetUser(projectId, email),
517
+ staleTime: 5 * 6e4,
518
+ enabled: !!email
519
+ });
520
+ const widgetCategoriesOptions = (projectId, parentId) => queryOptions({
521
+ queryKey: [
522
+ "widget-categories",
523
+ projectId,
524
+ parentId ?? "root"
525
+ ],
526
+ queryFn: () => getCategories(projectId, parentId),
527
+ staleTime: 5 * 6e4
528
+ });
529
+ //#endregion
530
+ //#region src/client-metadata.ts
531
+ const STORAGE_PREFIX = "reqdesk_diag_";
532
+ const DEFAULT_PREFS = {
533
+ screenResolution: false,
534
+ deviceType: false,
535
+ timezone: false,
536
+ referrerUrl: false,
537
+ language: false,
538
+ platform: false
539
+ };
540
+ /** Always collected — minimal, non-sensitive */
541
+ function collectMinimalMetadata() {
542
+ const meta = {};
543
+ try {
544
+ meta.pageUrl = window.location.href;
545
+ meta.userAgent = navigator.userAgent;
546
+ } catch {}
547
+ return meta;
548
+ }
549
+ /** Full diagnostic set — only included for opted-in fields */
550
+ function collectDiagnosticMetadata(prefs) {
551
+ const meta = {};
552
+ try {
553
+ if (prefs.screenResolution) meta.screenResolution = `${screen.width}x${screen.height}`;
554
+ if (prefs.deviceType) meta.deviceType = detectDeviceType();
555
+ if (prefs.timezone) meta.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
556
+ if (prefs.referrerUrl && document.referrer) meta.referrerUrl = document.referrer;
557
+ if (prefs.language) meta.language = navigator.language;
558
+ if (prefs.platform) meta.platform = navigator.platform;
559
+ } catch {}
560
+ return meta;
561
+ }
562
+ /** Combine minimal + opted-in diagnostic metadata */
563
+ function collectAllMetadata(apiKey) {
564
+ const prefs = getMetadataPreferences(apiKey);
565
+ return {
566
+ ...collectMinimalMetadata(),
567
+ ...collectDiagnosticMetadata(prefs)
568
+ };
569
+ }
570
+ function getMetadataPreferences(apiKey) {
571
+ try {
572
+ const raw = localStorage.getItem(`${STORAGE_PREFIX}${apiKey}`);
573
+ if (raw) return {
574
+ ...DEFAULT_PREFS,
575
+ ...JSON.parse(raw)
576
+ };
577
+ } catch {}
578
+ return { ...DEFAULT_PREFS };
579
+ }
580
+ function saveMetadataPreferences(apiKey, prefs) {
581
+ try {
582
+ localStorage.setItem(`${STORAGE_PREFIX}${apiKey}`, JSON.stringify(prefs));
583
+ } catch {}
584
+ }
585
+ function detectDeviceType() {
586
+ const ua = navigator.userAgent.toLowerCase();
587
+ if (/tablet|ipad|playbook|silk/i.test(ua)) return "tablet";
588
+ if (/mobile|iphone|ipod|android.*mobile|windows phone/i.test(ua)) return "mobile";
589
+ return "desktop";
590
+ }
591
+ const DIAGNOSTIC_FIELDS = [
592
+ {
593
+ key: "screenResolution",
594
+ labelKey: "diag.screenResolution"
595
+ },
596
+ {
597
+ key: "deviceType",
598
+ labelKey: "diag.deviceType"
599
+ },
600
+ {
601
+ key: "timezone",
602
+ labelKey: "diag.timezone"
603
+ },
604
+ {
605
+ key: "referrerUrl",
606
+ labelKey: "diag.referrerUrl"
607
+ },
608
+ {
609
+ key: "language",
610
+ labelKey: "diag.language"
611
+ },
612
+ {
613
+ key: "platform",
614
+ labelKey: "diag.platform"
615
+ }
616
+ ];
617
+ //#endregion
483
618
  //#region src/react/views/SubmitTicketView.tsx
484
619
  const translations$4 = {
485
620
  en,
@@ -529,6 +664,19 @@ function SubmitTicketView({ projectId, onSuccess, onError, isAuthenticated, user
529
664
  const [uploadProgress, setUploadProgress] = useState(null);
530
665
  const [success, setSuccess] = useState(null);
531
666
  const [rememberEmail, setRememberEmail] = useState(!!savedEmail);
667
+ const [categoryPath, setCategoryPath] = useState([]);
668
+ const [selectedCategory, setSelectedCategory] = useState(null);
669
+ const { data: categories = [] } = useQuery(widgetCategoriesOptions(projectId, categoryPath.length > 0 ? categoryPath[categoryPath.length - 1].id : null));
670
+ const [diagOpen, setDiagOpen] = useState(false);
671
+ const [diagPrefs, setDiagPrefs] = useState(getMetadataPreferences(ctx.apiKey));
672
+ const diagValues = collectDiagnosticMetadata({
673
+ screenResolution: true,
674
+ deviceType: true,
675
+ timezone: true,
676
+ referrerUrl: true,
677
+ language: true,
678
+ platform: true
679
+ });
532
680
  const t = useCallback((key) => {
533
681
  if (ctx.translations?.[key]) return ctx.translations[key];
534
682
  return (translations$4[ctx.language] ?? translations$4.en)[key] ?? key;
@@ -615,12 +763,15 @@ function SubmitTicketView({ projectId, onSuccess, onError, isAuthenticated, user
615
763
  }
616
764
  setErrors({});
617
765
  if (rememberEmail && email) saveWidgetEmail(ctx.apiKey, email);
766
+ saveMetadataPreferences(ctx.apiKey, diagPrefs);
618
767
  const validFiles = files.filter((f) => !f.error);
619
768
  const data = {
620
769
  title,
621
770
  description: formData.get("description")?.trim() || void 0,
622
771
  email,
623
- priority: formData.get("priority") ?? "medium"
772
+ priority: formData.get("priority") ?? "medium",
773
+ categoryId: selectedCategory?.id,
774
+ clientMetadata: collectAllMetadata(ctx.apiKey)
624
775
  };
625
776
  submitMutation.mutate({
626
777
  data,
@@ -801,6 +952,97 @@ function SubmitTicketView({ projectId, onSuccess, onError, isAuthenticated, user
801
952
  ]
802
953
  })]
803
954
  }),
955
+ /* @__PURE__ */ jsxs("div", {
956
+ className: "rqd-form-group",
957
+ children: [/* @__PURE__ */ jsx("label", {
958
+ className: "rqd-label",
959
+ children: t("form.categorySelected")
960
+ }), selectedCategory ? /* @__PURE__ */ jsxs("div", {
961
+ className: "rqd-category-selected",
962
+ children: [/* @__PURE__ */ jsxs("span", { children: [
963
+ categoryPath.map((c) => c.name).join(" > "),
964
+ categoryPath.length > 0 ? " > " : "",
965
+ selectedCategory.name
966
+ ] }), /* @__PURE__ */ jsx("button", {
967
+ type: "button",
968
+ onClick: () => {
969
+ setSelectedCategory(null);
970
+ setCategoryPath([]);
971
+ },
972
+ children: "×"
973
+ })]
974
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [categoryPath.length > 0 && /* @__PURE__ */ jsxs("div", {
975
+ className: "rqd-category-breadcrumb",
976
+ children: [/* @__PURE__ */ jsx("button", {
977
+ type: "button",
978
+ onClick: () => setCategoryPath([]),
979
+ children: t("form.categoryPlaceholder")
980
+ }), categoryPath.map((c, i) => /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("span", {
981
+ className: "rqd-category-breadcrumb-sep",
982
+ children: "›"
983
+ }), /* @__PURE__ */ jsx("button", {
984
+ type: "button",
985
+ onClick: () => setCategoryPath((prev) => prev.slice(0, i + 1)),
986
+ children: c.name
987
+ })] }, c.id))]
988
+ }), /* @__PURE__ */ jsxs("div", {
989
+ className: "rqd-category-list",
990
+ children: [categories.map((cat) => /* @__PURE__ */ jsxs("button", {
991
+ type: "button",
992
+ className: "rqd-category-item",
993
+ onClick: () => {
994
+ if (cat.hasChildren) setCategoryPath((prev) => [...prev, cat]);
995
+ else setSelectedCategory(cat);
996
+ },
997
+ children: [cat.name, cat.hasChildren && /* @__PURE__ */ jsx("span", {
998
+ className: "rqd-category-item-chevron",
999
+ children: "›"
1000
+ })]
1001
+ }, cat.id)), categories.length === 0 && categoryPath.length === 0 && /* @__PURE__ */ jsx("span", {
1002
+ style: {
1003
+ fontSize: 13,
1004
+ color: "var(--rqd-text-secondary)"
1005
+ },
1006
+ children: t("form.categoryPlaceholder")
1007
+ })]
1008
+ })] })]
1009
+ }),
1010
+ /* @__PURE__ */ jsxs("div", {
1011
+ className: "rqd-form-group",
1012
+ children: [/* @__PURE__ */ jsxs("button", {
1013
+ type: "button",
1014
+ className: "rqd-diag-toggle",
1015
+ onClick: () => setDiagOpen((prev) => !prev),
1016
+ children: [/* @__PURE__ */ jsx("span", { children: t("diag.title") }), /* @__PURE__ */ jsx("span", { children: diagOpen ? "▲" : "▼" })]
1017
+ }), diagOpen && /* @__PURE__ */ jsxs("div", {
1018
+ className: "rqd-diag-panel",
1019
+ children: [/* @__PURE__ */ jsx("p", {
1020
+ style: {
1021
+ fontSize: 12,
1022
+ color: "var(--rqd-text-secondary)",
1023
+ margin: "0 0 6px"
1024
+ },
1025
+ children: t("diag.hint")
1026
+ }), DIAGNOSTIC_FIELDS.map((field) => /* @__PURE__ */ jsxs("label", {
1027
+ className: "rqd-diag-item",
1028
+ children: [
1029
+ /* @__PURE__ */ jsx("input", {
1030
+ type: "checkbox",
1031
+ checked: diagPrefs[field.key],
1032
+ onChange: (e) => setDiagPrefs((prev) => ({
1033
+ ...prev,
1034
+ [field.key]: e.target.checked
1035
+ }))
1036
+ }),
1037
+ /* @__PURE__ */ jsx("span", { children: t(field.labelKey) }),
1038
+ /* @__PURE__ */ jsx("span", {
1039
+ className: "rqd-diag-item-value",
1040
+ children: diagValues[field.key] ?? "—"
1041
+ })
1042
+ ]
1043
+ }, field.key))]
1044
+ })]
1045
+ }),
804
1046
  /* @__PURE__ */ jsxs("div", {
805
1047
  className: "rqd-form-group",
806
1048
  children: [
@@ -864,35 +1106,6 @@ function SubmitTicketView({ projectId, onSuccess, onError, isAuthenticated, user
864
1106
  });
865
1107
  }
866
1108
  //#endregion
867
- //#region src/react/queries.ts
868
- const widgetTicketDetailOptions = (ticketId) => queryOptions({
869
- queryKey: ["widget-ticket", ticketId],
870
- queryFn: () => getTicketDetail(ticketId),
871
- staleTime: 6e4,
872
- enabled: !!ticketId
873
- });
874
- const widgetMyTicketsOptions = (projectId, userId) => queryOptions({
875
- queryKey: [
876
- "widget-tickets",
877
- projectId,
878
- userId
879
- ],
880
- queryFn: () => listMyTickets(projectId, userId),
881
- staleTime: 3e4,
882
- placeholderData: keepPreviousData,
883
- enabled: !!userId
884
- });
885
- const widgetUserOptions = (projectId, email) => queryOptions({
886
- queryKey: [
887
- "widget-user",
888
- projectId,
889
- email
890
- ],
891
- queryFn: () => resolveWidgetUser(projectId, email),
892
- staleTime: 5 * 6e4,
893
- enabled: !!email
894
- });
895
- //#endregion
896
1109
  //#region src/react/views/MyTicketsView.tsx
897
1110
  const translations$3 = {
898
1111
  en,
@@ -1074,6 +1287,24 @@ function TicketDetailView({ ticketId, onBack }) {
1074
1287
  if (context?.previous) queryClient.setQueryData(["widget-ticket", ticketId], context.previous);
1075
1288
  }
1076
1289
  });
1290
+ const resolveMutation = useMutation({
1291
+ mutationFn: () => closeTicket(ticketId),
1292
+ onMutate: async () => {
1293
+ await queryClient.cancelQueries({ queryKey: ["widget-ticket", ticketId] });
1294
+ const previous = queryClient.getQueryData(["widget-ticket", ticketId]);
1295
+ if (previous) queryClient.setQueryData(["widget-ticket", ticketId], {
1296
+ ...previous,
1297
+ status: "resolved"
1298
+ });
1299
+ return { previous };
1300
+ },
1301
+ onSuccess: () => {
1302
+ queryClient.invalidateQueries({ queryKey: ["widget-ticket", ticketId] });
1303
+ },
1304
+ onError: (_err, _vars, context) => {
1305
+ if (context?.previous) queryClient.setQueryData(["widget-ticket", ticketId], context.previous);
1306
+ }
1307
+ });
1077
1308
  if (isLoading) return /* @__PURE__ */ jsx("div", {
1078
1309
  className: "rqd-loading",
1079
1310
  children: t("detail.loading")
@@ -1182,11 +1413,28 @@ function TicketDetailView({ ticketId, onBack }) {
1182
1413
  onChange: (e) => setReplyBody(e.target.value),
1183
1414
  placeholder: t("detail.replyPlaceholder"),
1184
1415
  rows: 3
1185
- }), /* @__PURE__ */ jsx("button", {
1186
- className: "rqd-btn rqd-btn-primary",
1187
- onClick: () => replyMutation.mutate(replyBody.trim()),
1188
- disabled: !replyBody.trim() || replyMutation.isPending,
1189
- children: replyMutation.isPending ? t("detail.sending") : t("detail.sendReply")
1416
+ }), /* @__PURE__ */ jsxs("div", {
1417
+ style: {
1418
+ display: "flex",
1419
+ gap: 8
1420
+ },
1421
+ children: [/* @__PURE__ */ jsx("button", {
1422
+ className: "rqd-btn rqd-btn-primary",
1423
+ style: { flex: 1 },
1424
+ onClick: () => replyMutation.mutate(replyBody.trim()),
1425
+ disabled: !replyBody.trim() || replyMutation.isPending,
1426
+ children: replyMutation.isPending ? t("detail.sending") : t("detail.sendReply")
1427
+ }), ticket.status !== "resolved" && ticket.status !== "closed" && /* @__PURE__ */ jsx("button", {
1428
+ className: "rqd-btn rqd-btn-secondary",
1429
+ style: {
1430
+ flex: 0,
1431
+ whiteSpace: "nowrap",
1432
+ padding: "10px 16px"
1433
+ },
1434
+ onClick: () => resolveMutation.mutate(),
1435
+ disabled: resolveMutation.isPending,
1436
+ children: resolveMutation.isPending ? t("detail.resolving") : t("detail.resolve")
1437
+ })]
1190
1438
  })]
1191
1439
  })
1192
1440
  ] });
@@ -1313,7 +1561,7 @@ function FloatingWidget({ position = "bottom-right", contained = false, onTicket
1313
1561
  return (translations[activeLang] ?? translations.en)[key] ?? key;
1314
1562
  }, [activeLang, ctx.translations]);
1315
1563
  const isRtl = activeLang === "ar";
1316
- const cssVars = themeToVars(activeTheme);
1564
+ const cssVars = themeToStyle(activeTheme);
1317
1565
  const posClass = `rqd-${position}`;
1318
1566
  const containedClass = contained ? " rqd-contained" : "";
1319
1567
  const brandName = ctx.theme?.brandName;
@@ -1555,7 +1803,7 @@ function FloatingWidget({ position = "bottom-right", contained = false, onTicket
1555
1803
  const canGoBack = view !== "home";
1556
1804
  const goBackTarget = view === "ticket-detail" ? "my-tickets" : "home";
1557
1805
  return /* @__PURE__ */ jsx(ShadowRoot, { children: /* @__PURE__ */ jsxs("div", {
1558
- style: { cssText: cssVars },
1806
+ style: cssVars,
1559
1807
  ...isRtl ? { dir: "rtl" } : {},
1560
1808
  children: [/* @__PURE__ */ jsx("button", {
1561
1809
  className: `rqd-fab ${posClass}${containedClass}`,