reflet-sdk 0.6.4 → 0.6.6

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +100 -2
  3. package/dist/{client-DAdWdIFY.js → client-oFFfRxAM.js} +135 -59
  4. package/dist/client-oFFfRxAM.js.map +1 -0
  5. package/dist/client-transport-B7BdVU7J.d.ts +150 -0
  6. package/dist/client-transport-B7BdVU7J.d.ts.map +1 -0
  7. package/dist/{devtools-layer-DgmM3RhW.js → devtools-layer-B91EVgqg.js} +417 -916
  8. package/dist/devtools-layer-B91EVgqg.js.map +1 -0
  9. package/dist/devtools-next.js +1 -1
  10. package/dist/devtools-vite.js +2 -2
  11. package/dist/feedback.d.ts +7 -1
  12. package/dist/feedback.d.ts.map +1 -1
  13. package/dist/feedback.js +105 -57
  14. package/dist/feedback.js.map +1 -1
  15. package/dist/{handler-BSNmdHXv.js → handler-yA_knIf3.js} +4 -4
  16. package/dist/{handler-BSNmdHXv.js.map → handler-yA_knIf3.js.map} +1 -1
  17. package/dist/index-Buum9tIY.d.ts +230 -0
  18. package/dist/index-Buum9tIY.d.ts.map +1 -0
  19. package/dist/index.d.ts +3 -3
  20. package/dist/index.js +1 -1
  21. package/dist/options-CIBazDPp.d.ts.map +1 -1
  22. package/dist/protocol-vJhQHTcB.js +390 -0
  23. package/dist/protocol-vJhQHTcB.js.map +1 -0
  24. package/dist/{react-context-Caq1fodf.js → react-context-Kazo_JCz.js} +3 -14
  25. package/dist/react-context-Kazo_JCz.js.map +1 -0
  26. package/dist/react.d.ts +43 -3
  27. package/dist/react.d.ts.map +1 -1
  28. package/dist/react.js +389 -3
  29. package/dist/react.js.map +1 -1
  30. package/dist/selection-outline-NzAAAZXR.js +51 -0
  31. package/dist/selection-outline-NzAAAZXR.js.map +1 -0
  32. package/dist/server.js +1 -1
  33. package/dist/server.js.map +1 -1
  34. package/dist/sessions-panel-DVyWzZ3C.js +6011 -0
  35. package/dist/sessions-panel-DVyWzZ3C.js.map +1 -0
  36. package/dist/{submit-Cd-g1Dtg.js → submit-DYGZQVdG.js} +49 -74
  37. package/dist/submit-DYGZQVdG.js.map +1 -0
  38. package/dist/surveys.d.ts +3 -0
  39. package/dist/surveys.js +2 -0
  40. package/dist/team-activation-DCqh71bl.js +24 -0
  41. package/dist/team-activation-DCqh71bl.js.map +1 -0
  42. package/dist/team-layer-F-9BE4hc.js +423 -0
  43. package/dist/team-layer-F-9BE4hc.js.map +1 -0
  44. package/dist/{types-DnaDEN9Y.d.ts → types-B3XiHPqy.d.ts} +4 -9
  45. package/dist/types-B3XiHPqy.d.ts.map +1 -0
  46. package/dist/use-survey-session-LK6sN1Fg.js +1247 -0
  47. package/dist/use-survey-session-LK6sN1Fg.js.map +1 -0
  48. package/dist/z-index-CwokqVq_.js +15 -0
  49. package/dist/z-index-CwokqVq_.js.map +1 -0
  50. package/package.json +6 -2
  51. package/dist/client-DAdWdIFY.js.map +0 -1
  52. package/dist/client-DF1fdsSC.d.ts +0 -131
  53. package/dist/client-DF1fdsSC.d.ts.map +0 -1
  54. package/dist/devtools-layer-DgmM3RhW.js.map +0 -1
  55. package/dist/protocol-ClqTFzwR.js +0 -35
  56. package/dist/protocol-ClqTFzwR.js.map +0 -1
  57. package/dist/react-context-Caq1fodf.js.map +0 -1
  58. package/dist/submit-Cd-g1Dtg.js.map +0 -1
  59. package/dist/types-DnaDEN9Y.d.ts.map +0 -1
package/dist/react.js CHANGED
@@ -1,7 +1,115 @@
1
- import { n as Reflet } from "./client-DAdWdIFY.js";
2
- import { a as REFLET_Z_INDEX, i as useRefletContext, n as RefletProvider, r as useRefletClient, t as RefletContext } from "./react-context-Caq1fodf.js";
1
+ import { i as surveyRegistryKey, n as Reflet, r as surveyControllers } from "./client-oFFfRxAM.js";
2
+ import { a as SurveySession, c as displayOf, i as SurveyCard, n as useSurveySession, o as previewTransport, r as createRefletSurveyTransport, t as useSessionHandle } from "./use-survey-session-LK6sN1Fg.js";
3
+ import { i as useRefletContext, n as RefletProvider, r as useRefletClient, t as RefletContext } from "./react-context-Kazo_JCz.js";
4
+ import { t as REFLET_Z_INDEX } from "./z-index-CwokqVq_.js";
3
5
  import { cloneElement, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
+ import { createPortal } from "react-dom";
8
+ //#region ../survey-core/src/targeting.ts
9
+ const PATTERN_SEPARATOR = /[,\n]/;
10
+ const REGEX_SPECIAL_CHARS = /[.+?^${}()|[\]\\]/g;
11
+ const WILDCARD = /\*/g;
12
+ const TRAILING_SLASHES = /\/+$/;
13
+ const stripTrailingSlash = (path) => path.replace(TRAILING_SLASHES, "") || "/";
14
+ const patternToRegex = (pattern) => {
15
+ const escaped = stripTrailingSlash(pattern).replace(REGEX_SPECIAL_CHARS, "\\$&").replace(WILDCARD, ".*");
16
+ return new RegExp(`^${escaped}/?$`, "i");
17
+ };
18
+ const HASH_ROUTE_PREFIX = "#/";
19
+ const HASH_ROUTE_QUERY = /\?.*$/;
20
+ const hashRoutePathOf = (url) => url.hash.startsWith(HASH_ROUTE_PREFIX) ? [stripTrailingSlash(url.hash.slice(1).replace(HASH_ROUTE_QUERY, ""))] : [];
21
+ /**
22
+ * Comma- or newline-separated patterns with `*` wildcards. A pattern starting
23
+ * with `/` matches the path (or a `#/` hash route); one with a scheme matches
24
+ * the full URL; anything else matches host and path. Empty matches every page.
25
+ */
26
+ const matchesPageUrl = (patterns, href) => {
27
+ const list = (patterns ?? "").split(PATTERN_SEPARATOR).map((pattern) => pattern.trim()).filter(Boolean);
28
+ if (list.length === 0) return true;
29
+ let url;
30
+ try {
31
+ url = new URL(href);
32
+ } catch {
33
+ return false;
34
+ }
35
+ const path = stripTrailingSlash(url.pathname);
36
+ const paths = [path, ...hashRoutePathOf(url)];
37
+ const candidatesFor = (pattern) => {
38
+ if (pattern.startsWith("/")) return paths;
39
+ return pattern.includes("://") ? [`${url.origin}${path}`] : [`${url.host}${path}`];
40
+ };
41
+ return list.some((pattern) => {
42
+ const regex = patternToRegex(pattern);
43
+ return candidatesFor(pattern).some((candidate) => regex.test(candidate));
44
+ });
45
+ };
46
+ //#endregion
47
+ //#region ../survey-core/src/triggers.ts
48
+ const URL_POLL_INTERVAL_MS = 1e3;
49
+ /**
50
+ * Arms every survey's trigger on the page: page visits (including client-side
51
+ * navigation), time on page, exit intent, custom events and submitted feedback.
52
+ * Each survey fires at most once per page session.
53
+ */
54
+ const createSurveyTriggers = (options) => {
55
+ const win = options.window ?? window;
56
+ const fired = /* @__PURE__ */ new Set();
57
+ const timers = /* @__PURE__ */ new Map();
58
+ let surveys = options.surveys;
59
+ let lastHref = win.location.href;
60
+ const onCurrentPage = (survey) => matchesPageUrl(survey.triggerConfig?.pageUrl, win.location.href);
61
+ const fire = (survey) => {
62
+ if (fired.has(survey._id) || !onCurrentPage(survey)) return;
63
+ if (options.onTrigger(survey)) fired.add(survey._id);
64
+ };
65
+ const clearTimers = () => {
66
+ for (const timer of timers.values()) win.clearTimeout(timer);
67
+ timers.clear();
68
+ };
69
+ const armPage = () => {
70
+ clearTimers();
71
+ for (const survey of surveys) {
72
+ if (fired.has(survey._id) || !onCurrentPage(survey)) continue;
73
+ if (survey.triggerType === "page_visit") fire(survey);
74
+ if (survey.triggerType === "time_delay") {
75
+ const delayMs = survey.triggerConfig?.delayMs ?? 5e3;
76
+ timers.set(survey._id, win.setTimeout(() => fire(survey), delayMs));
77
+ }
78
+ }
79
+ };
80
+ const fireAll = (matches) => {
81
+ for (const survey of surveys) if (matches(survey)) fire(survey);
82
+ };
83
+ const onMouseOut = (event) => {
84
+ if (event.relatedTarget === null && event.clientY <= 0) fireAll((survey) => survey.triggerType === "exit_intent");
85
+ };
86
+ const onNavigation = () => {
87
+ if (win.location.href === lastHref) return;
88
+ lastHref = win.location.href;
89
+ armPage();
90
+ };
91
+ win.document.addEventListener("mouseout", onMouseOut);
92
+ win.addEventListener("popstate", onNavigation);
93
+ win.addEventListener("hashchange", onNavigation);
94
+ const urlPoll = win.setInterval(onNavigation, URL_POLL_INTERVAL_MS);
95
+ armPage();
96
+ return {
97
+ destroy: () => {
98
+ clearTimers();
99
+ win.clearInterval(urlPoll);
100
+ win.document.removeEventListener("mouseout", onMouseOut);
101
+ win.removeEventListener("popstate", onNavigation);
102
+ win.removeEventListener("hashchange", onNavigation);
103
+ },
104
+ notifyFeedbackSubmitted: () => fireAll((survey) => survey.triggerType === "feedback_submitted"),
105
+ setSurveys: (next) => {
106
+ surveys = next;
107
+ armPage();
108
+ },
109
+ track: (eventName) => fireAll((survey) => survey.triggerType === "event" && survey.triggerConfig?.eventName === eventName)
110
+ };
111
+ };
112
+ //#endregion
5
113
  //#region src/react-changelog-widget.tsx
6
114
  /**
7
115
  * React component that wraps the Reflet Changelog Widget.
@@ -1217,6 +1325,284 @@ function useUnreadChangelogCount(publicKey) {
1217
1325
  };
1218
1326
  }
1219
1327
  //#endregion
1220
- export { ChangelogWidget, FeedbackButton, FeedbackDialog, Reflet, RefletContext, RefletProvider, useAddComment, useChangelog, useComments, useCreateFeedback, useFeedback, useFeedbackList, useOrganizationConfig, useRefletClient, useRefletContext, useRoadmap, useSubscription, useUnreadChangelogCount, useVote };
1328
+ //#region src/surveys/controller.ts
1329
+ /**
1330
+ * Owns in-app delivery for one Reflet project: eligible surveys, armed
1331
+ * triggers and the survey on screen. Lives outside React so signals sent
1332
+ * before `<RefletSurveys />` has loaded surveys are queued, not lost.
1333
+ */
1334
+ var SurveyController = class {
1335
+ callbacks = {};
1336
+ active = null;
1337
+ client = null;
1338
+ generation = 0;
1339
+ listeners = /* @__PURE__ */ new Set();
1340
+ pending = [];
1341
+ surveys = null;
1342
+ triggers = null;
1343
+ subscribe = (listener) => {
1344
+ this.listeners.add(listener);
1345
+ return () => this.listeners.delete(listener);
1346
+ };
1347
+ getActiveSession = () => this.active;
1348
+ attach(client) {
1349
+ this.generation += 1;
1350
+ const generation = this.generation;
1351
+ this.client = client;
1352
+ this.triggers = createSurveyTriggers({
1353
+ onTrigger: (survey) => this.activate(survey),
1354
+ surveys: []
1355
+ });
1356
+ this.refresh();
1357
+ return () => {
1358
+ if (generation !== this.generation) return;
1359
+ this.generation += 1;
1360
+ this.triggers?.destroy();
1361
+ this.triggers = null;
1362
+ this.client = null;
1363
+ this.surveys = null;
1364
+ this.active?.dismiss();
1365
+ this.setActive(null);
1366
+ };
1367
+ }
1368
+ track(eventName) {
1369
+ if (this.triggers && this.surveys) {
1370
+ this.triggers.track(eventName);
1371
+ return;
1372
+ }
1373
+ this.enqueue({
1374
+ eventName,
1375
+ kind: "track"
1376
+ });
1377
+ }
1378
+ notifyFeedbackSubmitted() {
1379
+ if (this.triggers && this.surveys) {
1380
+ this.triggers.notifyFeedbackSubmitted();
1381
+ return;
1382
+ }
1383
+ this.enqueue({ kind: "feedback_submitted" });
1384
+ }
1385
+ showSurvey(surveyId) {
1386
+ if (!this.surveys) {
1387
+ this.enqueue({
1388
+ kind: "show",
1389
+ surveyId
1390
+ });
1391
+ return;
1392
+ }
1393
+ const survey = this.surveys.find((candidate) => candidate._id === surveyId);
1394
+ if (survey) this.activate(survey);
1395
+ }
1396
+ dismissSurvey() {
1397
+ this.active?.dismiss();
1398
+ }
1399
+ async refresh() {
1400
+ const client = this.client;
1401
+ const generation = this.generation;
1402
+ if (!client) return;
1403
+ let surveys;
1404
+ try {
1405
+ surveys = await client.getEligibleSurveys();
1406
+ } catch {
1407
+ surveys = [];
1408
+ }
1409
+ if (generation !== this.generation) return;
1410
+ this.surveys = surveys;
1411
+ this.triggers?.setSurveys(surveys);
1412
+ const pending = this.pending;
1413
+ this.pending = [];
1414
+ for (const call of pending) if (call.kind === "track") this.track(call.eventName);
1415
+ else if (call.kind === "feedback_submitted") this.notifyFeedbackSubmitted();
1416
+ else this.showSurvey(call.surveyId);
1417
+ }
1418
+ enqueue(call) {
1419
+ this.pending = [...this.pending, call].slice(-50);
1420
+ }
1421
+ activate(survey) {
1422
+ const client = this.client;
1423
+ if (this.active || !client) return false;
1424
+ const session = new SurveySession({
1425
+ callbacks: {
1426
+ onAnswer: (event) => this.callbacks.onSurveyAnswer?.(event),
1427
+ onComplete: (event) => this.callbacks.onSurveyComplete?.(event),
1428
+ onDismiss: (event) => this.callbacks.onSurveyDismiss?.(event),
1429
+ onStart: (event) => this.callbacks.onSurveyStart?.(event)
1430
+ },
1431
+ survey,
1432
+ transport: createRefletSurveyTransport(client)
1433
+ });
1434
+ const unsubscribe = session.subscribe(() => {
1435
+ if (session.getSnapshot().phase !== "dismissed") return;
1436
+ unsubscribe();
1437
+ if (this.active === session) {
1438
+ this.setActive(null);
1439
+ this.refresh();
1440
+ }
1441
+ });
1442
+ this.setActive(session);
1443
+ session.start().then(() => {
1444
+ if (session.getSnapshot().phase === "start_failed" && this.active === session) {
1445
+ unsubscribe();
1446
+ this.setActive(null);
1447
+ }
1448
+ });
1449
+ return true;
1450
+ }
1451
+ setActive(session) {
1452
+ this.active = session;
1453
+ for (const listener of this.listeners) listener();
1454
+ }
1455
+ };
1456
+ /** Shared by `<RefletSurveys />`, `useRefletSurveys()` and the client's feedback signal. */
1457
+ const surveyControllerFor = (publicKey, baseUrl) => {
1458
+ const key = surveyRegistryKey(publicKey, baseUrl ?? "https://harmless-clam-802.convex.site");
1459
+ let controller = surveyControllers.get(key);
1460
+ if (!controller) {
1461
+ controller = new SurveyController();
1462
+ surveyControllers.set(key, controller);
1463
+ }
1464
+ return controller;
1465
+ };
1466
+ //#endregion
1467
+ //#region src/surveys/survey-portal.tsx
1468
+ const SURVEY_HOST_ATTRIBUTE = "data-reflet-survey";
1469
+ const LAYER_STYLES = `
1470
+ :host { all: initial; }
1471
+ .rfs-layer {
1472
+ position: fixed;
1473
+ z-index: ${REFLET_Z_INDEX.surveyLayer};
1474
+ bottom: 20px;
1475
+ width: min(360px, calc(100vw - 32px));
1476
+ }
1477
+ .rfs-layer[data-position="bottom_right"] { right: 20px; }
1478
+ .rfs-layer[data-position="bottom_left"] { left: 20px; }
1479
+ .rfs-layer[data-position="center"] {
1480
+ inset: 0;
1481
+ display: grid;
1482
+ place-items: center;
1483
+ width: auto;
1484
+ padding: 16px;
1485
+ background: rgb(15 15 15 / 28%);
1486
+ animation: rfs-layer-fade 240ms ease-out;
1487
+ }
1488
+ .rfs-layer[data-position="center"] > .rf-survey { max-width: 420px; }
1489
+ @media (max-width: 480px) {
1490
+ .rfs-layer[data-position^="bottom"] { right: 16px; bottom: 16px; left: 16px; width: auto; }
1491
+ }
1492
+ @keyframes rfs-layer-fade { from { opacity: 0; } }
1493
+ @media (prefers-reduced-motion: reduce) { .rfs-layer { animation: none; } }
1494
+ `;
1495
+ /**
1496
+ * Hosts an in-app survey in its own shadow root so page CSS can't restyle it.
1497
+ * Direct DOM work is unavoidable: React can't create a shadow root declaratively.
1498
+ */
1499
+ function SurveyPortal({ children, position }) {
1500
+ const [shadowRoot, setShadowRoot] = useState(null);
1501
+ useEffect(() => {
1502
+ const host = document.createElement("div");
1503
+ host.setAttribute(SURVEY_HOST_ATTRIBUTE, "");
1504
+ const root = host.attachShadow({ mode: "open" });
1505
+ const style = document.createElement("style");
1506
+ style.textContent = LAYER_STYLES;
1507
+ root.append(style);
1508
+ document.body.append(host);
1509
+ setShadowRoot(root);
1510
+ return () => host.remove();
1511
+ }, []);
1512
+ if (!shadowRoot) return null;
1513
+ return createPortal(/* @__PURE__ */ jsx("div", {
1514
+ className: "rfs-layer",
1515
+ "data-position": position,
1516
+ children
1517
+ }), shadowRoot);
1518
+ }
1519
+ //#endregion
1520
+ //#region src/surveys/reflet-surveys.tsx
1521
+ const ENDING_AUTO_CLOSE_MS = 6e3;
1522
+ function useOrganizationColor(client, override) {
1523
+ const [organizationColor, setOrganizationColor] = useState();
1524
+ useEffect(() => {
1525
+ if (override) return;
1526
+ let cancelled = false;
1527
+ client.getConfig().then((config) => {
1528
+ if (!cancelled) setOrganizationColor(config.primaryColor);
1529
+ }).catch(() => void 0);
1530
+ return () => {
1531
+ cancelled = true;
1532
+ };
1533
+ }, [client, override]);
1534
+ return override ?? organizationColor;
1535
+ }
1536
+ function ActiveSurvey({ primaryColor, session, theme }) {
1537
+ const handle = useSessionHandle(session);
1538
+ const { ending, phase } = handle.snapshot;
1539
+ const endsQuietly = ending !== null && !(ending.buttonUrl || ending.buttonLabel);
1540
+ useEffect(() => {
1541
+ if (!endsQuietly) return;
1542
+ const timer = window.setTimeout(() => session.dismiss(), ENDING_AUTO_CLOSE_MS);
1543
+ return () => window.clearTimeout(timer);
1544
+ }, [endsQuietly, session]);
1545
+ if (phase !== "question" && phase !== "ending") return null;
1546
+ return /* @__PURE__ */ jsx(SurveyPortal, {
1547
+ position: displayOf(session.survey.display).position ?? "bottom_right",
1548
+ children: /* @__PURE__ */ jsx(SurveyCard, {
1549
+ primaryColor,
1550
+ session: handle,
1551
+ theme,
1552
+ variant: "floating"
1553
+ })
1554
+ });
1555
+ }
1556
+ /**
1557
+ * In-app survey delivery. Mount once inside `RefletProvider`: it loads the
1558
+ * surveys this visitor is eligible for, arms their triggers and shows one at a time.
1559
+ */
1560
+ function RefletSurveys({ enabled = true, onSurveyAnswer, onSurveyComplete, onSurveyDismiss, onSurveyStart, primaryColor, theme = "auto" }) {
1561
+ const { baseUrl, client, publicKey } = useRefletContext();
1562
+ const controller = surveyControllerFor(publicKey, baseUrl);
1563
+ const color = useOrganizationColor(client, primaryColor);
1564
+ const session = useSyncExternalStore(controller.subscribe, controller.getActiveSession, () => null);
1565
+ useEffect(() => {
1566
+ controller.callbacks = {
1567
+ onSurveyAnswer,
1568
+ onSurveyComplete,
1569
+ onSurveyDismiss,
1570
+ onSurveyStart
1571
+ };
1572
+ });
1573
+ useEffect(() => {
1574
+ if (!enabled) return;
1575
+ return controller.attach(client);
1576
+ }, [
1577
+ client,
1578
+ controller,
1579
+ enabled
1580
+ ]);
1581
+ if (!session) return null;
1582
+ return /* @__PURE__ */ jsx(ActiveSurvey, {
1583
+ primaryColor: color,
1584
+ session,
1585
+ theme
1586
+ }, session.survey._id);
1587
+ }
1588
+ //#endregion
1589
+ //#region src/surveys/use-reflet-surveys.ts
1590
+ /**
1591
+ * Drives `<RefletSurveys />` from anywhere inside `RefletProvider`. Calls made
1592
+ * before surveys load are queued and replayed once they do.
1593
+ */
1594
+ function useRefletSurveys() {
1595
+ const { baseUrl, publicKey } = useRefletContext();
1596
+ const controller = surveyControllerFor(publicKey, baseUrl);
1597
+ const activeSurveyId = useSyncExternalStore(controller.subscribe, () => controller.getActiveSession()?.survey._id ?? null, () => null);
1598
+ return useMemo(() => ({
1599
+ activeSurveyId,
1600
+ dismissSurvey: () => controller.dismissSurvey(),
1601
+ showSurvey: (surveyId) => controller.showSurvey(surveyId),
1602
+ track: (eventName) => controller.track(eventName)
1603
+ }), [activeSurveyId, controller]);
1604
+ }
1605
+ //#endregion
1606
+ export { ChangelogWidget, FeedbackButton, FeedbackDialog, Reflet, RefletContext, RefletProvider, RefletSurveys, SurveyCard, SurveySession, createRefletSurveyTransport, previewTransport, useAddComment, useChangelog, useComments, useCreateFeedback, useFeedback, useFeedbackList, useOrganizationConfig, useRefletClient, useRefletContext, useRefletSurveys, useRoadmap, useSubscription, useSurveySession, useUnreadChangelogCount, useVote };
1221
1607
 
1222
1608
  //# sourceMappingURL=react.js.map