@guidelytech/react-native 0.9.0 → 0.11.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/index.cjs CHANGED
@@ -28,14 +28,18 @@ __export(index_exports, {
28
28
  FeatureValue: () => FeatureValue,
29
29
  FlagsProvider: () => FlagsProvider,
30
30
  GuidelyClient: () => GuidelyClient,
31
+ GuidelyMask: () => GuidelyMask,
31
32
  GuidelyOverlay: () => GuidelyOverlay,
32
33
  GuidelyProvider: () => GuidelyProvider,
33
34
  GuidelyScreenScope: () => GuidelyScreenScope,
34
35
  GuidelyTarget: () => GuidelyTarget,
36
+ HeatmapBuffer: () => HeatmapBuffer,
35
37
  InMemoryStickyBucketStore: () => import_flags6.InMemoryStickyBucketStore,
36
38
  Messenger: () => Messenger,
39
+ RageClickDetector: () => RageClickDetector,
37
40
  SDK_NAME: () => SDK_NAME,
38
41
  SDK_VERSION: () => SDK_VERSION,
42
+ TapTracker: () => TapTracker,
39
43
  TargetRegistry: () => TargetRegistry,
40
44
  createFlagsClient: () => import_flags6.createFlagsClient,
41
45
  createSseParser: () => createSseParser,
@@ -63,8 +67,8 @@ __export(index_exports, {
63
67
  useGuidelyTarget: () => useGuidelyTarget
64
68
  });
65
69
  module.exports = __toCommonJS(index_exports);
66
- var import_react9 = require("react");
67
- var import_react_native12 = require("react-native");
70
+ var import_react10 = require("react");
71
+ var import_react_native13 = require("react-native");
68
72
  var import_flags3 = require("@guidelytech/flags");
69
73
 
70
74
  // src/flags.tsx
@@ -156,7 +160,7 @@ function FlagsProvider({ children, client: external, payload, ...options }) {
156
160
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FlagsContext.Provider, { value, children });
157
161
  }
158
162
 
159
- // ../shared/dist/chunk-UW6NRYRY.js
163
+ // ../shared/dist/chunk-TGIUEYXM.js
160
164
  function fnv1a(input) {
161
165
  let h = 2166136261;
162
166
  for (let i = 0; i < input.length; i++) {
@@ -336,6 +340,8 @@ function resolveField(rule, ctx) {
336
340
  return ctx.now.getHours();
337
341
  case "rollout.percentage":
338
342
  return rolloutBucket(ctx.userId || ctx.anonymousId, ctx.rolloutSalt || "guidely");
343
+ case "experiment.bucket":
344
+ return rule.key ? rolloutBucket(ctx.userId || ctx.anonymousId, rule.key) : void 0;
339
345
  case "environment":
340
346
  return ctx.environment;
341
347
  case "locale":
@@ -346,6 +352,9 @@ function resolveField(rule, ctx) {
346
352
  return ctx.screen;
347
353
  case "app.version":
348
354
  return ctx.appVersion;
355
+ case "page.views":
356
+ case "feature.clicks":
357
+ return rule.key ? ctx.tagUsage?.[rule.key] ?? 0 : 0;
349
358
  default:
350
359
  return void 0;
351
360
  }
@@ -454,8 +463,8 @@ function sanitizeProperties(props) {
454
463
  for (const [k, v] of Object.entries(props)) {
455
464
  if (SENSITIVE_PROPERTY_PATTERN.test(k)) continue;
456
465
  if (typeof v === "function" || typeof v === "symbol") continue;
457
- const str = typeof v === "string" ? v : JSON.stringify(v ?? null);
458
- size += k.length + (str?.length ?? 0);
466
+ const str2 = typeof v === "string" ? v : JSON.stringify(v ?? null);
467
+ size += k.length + (str2?.length ?? 0);
459
468
  if (size > MAX_PROPERTY_BYTES) break;
460
469
  out[k.slice(0, 100)] = typeof v === "string" ? v.slice(0, 1e3) : v;
461
470
  }
@@ -612,53 +621,252 @@ function isArticleListed(collections, article, member) {
612
621
  }
613
622
  return true;
614
623
  }
624
+ var RC_MODULE_IDS = ["announcements", "search", "checklists", "guides", "articles", "links", "faqs"];
625
+ function orderedModules(modules) {
626
+ const known = (modules ?? []).filter((m, i, all) => RC_MODULE_IDS.includes(m.id) && all.findIndex((x) => x.id === m.id) === i);
627
+ const missing2 = RC_MODULE_IDS.filter((id) => !known.some((m) => m.id === id)).map((id) => ({ id, enabled: true }));
628
+ return [...known, ...missing2];
629
+ }
630
+ function visibleAnnouncements(items, member, now = Date.now()) {
631
+ return (items ?? []).filter((a) => {
632
+ const t = Date.parse(a.publishedAt);
633
+ return !Number.isNaN(t) && t <= now && matchesAudiences(a.audienceIds, member);
634
+ }).sort((a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt));
635
+ }
636
+ function unreadAnnouncements(items, lastSeenAt) {
637
+ const seen = lastSeenAt ? Date.parse(lastSeenAt) : NaN;
638
+ if (Number.isNaN(seen)) return items.length;
639
+ return items.filter((a) => Date.parse(a.publishedAt) > seen).length;
640
+ }
615
641
  var CAPTURE_SCREENSHOT_MAX_BYTES = 8 * 1024 * 1024;
616
-
617
- // ../sdk-core/dist/chunk-CYAG6ZYX.js
618
- function readJson(store, key, fallback) {
619
- const raw = store.get(key);
620
- if (raw == null) return fallback;
621
- try {
622
- return JSON.parse(raw);
623
- } catch {
624
- return fallback;
625
- }
642
+ var DEFAULT_LOCALE = "en";
643
+ function normalizeLocale(input) {
644
+ if (typeof input !== "string") return "";
645
+ const parts = input.trim().replace(/_/g, "-").split("-").filter(Boolean);
646
+ if (!parts.length || !/^[a-zA-Z]{2,3}$/.test(parts[0])) return "";
647
+ return parts.slice(0, 3).map((p, i) => i === 0 ? p.toLowerCase() : p.length === 4 ? p[0].toUpperCase() + p.slice(1).toLowerCase() : p.length === 2 ? p.toUpperCase() : p).join("-");
648
+ }
649
+ function baseLanguage(locale) {
650
+ return normalizeLocale(locale).split("-")[0] ?? "";
651
+ }
652
+ function resolveLocale(requested, available, defaultLocale = DEFAULT_LOCALE) {
653
+ const def = normalizeLocale(defaultLocale) || DEFAULT_LOCALE;
654
+ const avail = available.map(normalizeLocale).filter(Boolean);
655
+ const all = [def, ...avail.filter((a) => a.toLowerCase() !== def.toLowerCase())];
656
+ for (const raw of requested) {
657
+ const want = normalizeLocale(raw);
658
+ if (!want) continue;
659
+ const exact = all.find((a) => a.toLowerCase() === want.toLowerCase());
660
+ if (exact) return exact;
661
+ const base = baseLanguage(want);
662
+ const sameBase = all.find((a) => a.toLowerCase() === base) ?? all.find((a) => baseLanguage(a) === base);
663
+ if (sameBase) return sameBase;
664
+ }
665
+ return def;
666
+ }
667
+ function visitorLocales(explicit, traits, platform = []) {
668
+ const out = [];
669
+ const push = (v) => {
670
+ const n = normalizeLocale(v);
671
+ if (n && !out.includes(n)) out.push(n);
672
+ };
673
+ push(explicit);
674
+ push(traits?.language);
675
+ push(traits?.locale);
676
+ for (const p of platform) push(p);
677
+ return out;
626
678
  }
627
- function writeJson(store, key, value) {
628
- try {
629
- store.set(key, JSON.stringify(value));
630
- } catch {
631
- }
679
+ function guideLocales(config) {
680
+ const t = config.localization?.translations ?? {};
681
+ return Object.keys(t).filter((l) => Object.values(t[l] ?? {}).some((v) => typeof v === "string" && v.trim() !== ""));
682
+ }
683
+ function pick(map, key, fallback) {
684
+ const v = map?.[key];
685
+ return typeof v === "string" && v.trim() !== "" ? v : fallback;
686
+ }
687
+ function applyGuideTranslations(config, map) {
688
+ if (!map || !Object.keys(map).length) return config;
689
+ return {
690
+ ...config,
691
+ steps: config.steps.map((s) => ({
692
+ ...s,
693
+ title: pick(map, `steps.${s.id}.title`, s.title),
694
+ body: pick(map, `steps.${s.id}.body`, s.body),
695
+ customHtml: pick(map, `steps.${s.id}.customHtml`, s.customHtml),
696
+ imageAlt: pick(map, `steps.${s.id}.imageAlt`, s.imageAlt),
697
+ buttons: s.buttons.map((b) => ({ ...b, label: pick(map, `steps.${s.id}.buttons.${b.id}.label`, b.label) }))
698
+ })),
699
+ checklist: config.checklist ? {
700
+ ...config.checklist,
701
+ launcherLabel: pick(map, "checklist.launcherLabel", config.checklist.launcherLabel),
702
+ completionTitle: pick(map, "checklist.completionTitle", config.checklist.completionTitle),
703
+ completionBody: pick(map, "checklist.completionBody", config.checklist.completionBody),
704
+ tasks: config.checklist.tasks.map((t) => ({ ...t, title: pick(map, `checklist.tasks.${t.id}.title`, t.title), description: pick(map, `checklist.tasks.${t.id}.description`, t.description) }))
705
+ } : config.checklist,
706
+ survey: config.survey ? {
707
+ ...config.survey,
708
+ thankYouTitle: pick(map, "survey.thankYouTitle", config.survey.thankYouTitle),
709
+ thankYouBody: pick(map, "survey.thankYouBody", config.survey.thankYouBody),
710
+ questions: config.survey.questions.map((q) => ({
711
+ ...q,
712
+ title: pick(map, `survey.questions.${q.id}.title`, q.title),
713
+ description: pick(map, `survey.questions.${q.id}.description`, q.description),
714
+ placeholder: pick(map, `survey.questions.${q.id}.placeholder`, q.placeholder),
715
+ lowLabel: pick(map, `survey.questions.${q.id}.lowLabel`, q.lowLabel),
716
+ highLabel: pick(map, `survey.questions.${q.id}.highLabel`, q.highLabel),
717
+ options: q.options.map((o) => ({ ...o, label: pick(map, `survey.questions.${q.id}.options.${o.id}.label`, o.label) }))
718
+ }))
719
+ } : config.survey
720
+ };
721
+ }
722
+ function localizeGuide(guide, requested, defaultLocale = DEFAULT_LOCALE) {
723
+ const translations = guide.config.localization?.translations ?? {};
724
+ const locale = resolveLocale(requested, guideLocales(guide.config), defaultLocale);
725
+ const key = Object.keys(translations).find((l) => normalizeLocale(l).toLowerCase() === locale.toLowerCase());
726
+ if (!key) return { ...guide, locale };
727
+ return { ...guide, locale, config: applyGuideTranslations(guide.config, translations[key]) };
728
+ }
729
+ function resourceCenterLocales(rc) {
730
+ const set = new Set(Object.keys(rc.translations ?? {}));
731
+ for (const a of rc.articles) for (const l of Object.keys(a.translations ?? {})) set.add(l);
732
+ for (const a of rc.announcements ?? []) for (const l of Object.keys(a.translations ?? {})) set.add(l);
733
+ return [...set];
734
+ }
735
+ function translationFor(map, locale) {
736
+ if (!map) return void 0;
737
+ const key = Object.keys(map).find((l) => normalizeLocale(l).toLowerCase() === locale.toLowerCase());
738
+ return key ? map[key] : void 0;
739
+ }
740
+ function localizeArticle(a, locale) {
741
+ const t = translationFor(a.translations, locale);
742
+ if (!t) return a;
743
+ const body = t.body?.trim() ? t.body : a.body;
744
+ return { ...a, title: t.title?.trim() ? t.title : a.title, summary: t.summary?.trim() ? t.summary : a.summary, body, ...t.bodyOmitted ? { bodyOmitted: true } : {} };
745
+ }
746
+ function localizeAnnouncement(a, locale) {
747
+ const t = translationFor(a.translations, locale);
748
+ if (!t) return a;
749
+ return { ...a, title: t.title?.trim() ? t.title : a.title, body: t.body?.trim() ? t.body : a.body };
750
+ }
751
+ function localizeResourceCenter(rc, requested, defaultLocale = DEFAULT_LOCALE) {
752
+ const locale = resolveLocale(requested, resourceCenterLocales(rc), defaultLocale);
753
+ if (locale.toLowerCase() === normalizeLocale(defaultLocale).toLowerCase()) return { ...rc, locale };
754
+ const map = translationFor(rc.translations, locale);
755
+ return {
756
+ ...rc,
757
+ locale,
758
+ title: pick(map, "title", rc.title),
759
+ launcher: { ...rc.launcher, label: pick(map, "launcher.label", rc.launcher.label) },
760
+ collections: rc.collections.map((c) => ({ ...c, name: pick(map, `collections.${c.id}.name`, c.name), description: pick(map, `collections.${c.id}.description`, c.description) })),
761
+ links: rc.links.map((l) => ({ ...l, label: pick(map, `links.${l.id}.label`, l.label) })),
762
+ faqs: rc.faqs.map((f) => ({ ...f, question: pick(map, `faqs.${f.id}.question`, f.question), answer: pick(map, `faqs.${f.id}.answer`, f.answer) })),
763
+ articles: rc.articles.map((a) => localizeArticle(a, locale)),
764
+ announcements: rc.announcements?.map((a) => localizeAnnouncement(a, locale))
765
+ };
632
766
  }
633
- var KEYS = {
634
- anon: "guidely:anon",
635
- seen: "guidely:seen",
636
- config: (key) => `guidely:config:${key}`,
637
- etag: (key) => `guidely:etag:${key}`,
638
- state: (key) => `guidely:state:${key}`,
639
- user: (key) => `guidely:user:${key}`,
640
- queue: (key) => `guidely:queue:${key}`,
641
- session: "guidely:session",
642
- active: "guidely:active",
643
- authoring: "guidely:authoring",
644
- /** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
645
- capture: "guidely:capture",
646
- capturePending: "guidely:capture:pending",
647
- /** Messenger conversations this browser can read: [{ id, token }]. */
648
- conversations: (key) => `guidely:conversations:${key}`,
649
- /** Announcement ids opened in the messenger's News tab. */
650
- newsSeen: (key) => `guidely:news:${key}`
651
- };
652
767
 
653
- // ../sdk-core/dist/chunk-AKNE7D5M.js
768
+ // ../sdk-core/dist/chunk-JUSM7J5N.js
654
769
  var __defProp2 = Object.defineProperty;
655
770
  var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
656
771
  var __publicField2 = (obj, key, value) => __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
772
+ var MAX_RETRY_AFTER_MS = 60 * 60 * 1e3;
773
+ var OFFLINE_WAIT_MS = 3e4;
774
+ var ServerHealth = class {
775
+ constructor(opts = {}) {
776
+ __publicField2(this, "failures", 0);
777
+ __publicField2(this, "openUntil", 0);
778
+ __publicField2(this, "lastOkAt", 0);
779
+ __publicField2(this, "listeners", /* @__PURE__ */ new Set());
780
+ __publicField2(this, "threshold");
781
+ __publicField2(this, "baseMs");
782
+ __publicField2(this, "maxMs");
783
+ __publicField2(this, "now");
784
+ __publicField2(this, "random");
785
+ this.threshold = opts.threshold ?? 3;
786
+ this.baseMs = opts.baseMs ?? 5e3;
787
+ this.maxMs = opts.maxMs ?? 5 * 60 * 1e3;
788
+ this.now = opts.now ?? Date.now;
789
+ this.random = opts.random ?? Math.random;
790
+ }
791
+ /** The server answered (2xx, 3xx, or a 4xx other than 429): it is up. */
792
+ success() {
793
+ const wasDown = this.failures > 0 || this.openUntil > 0;
794
+ this.failures = 0;
795
+ this.openUntil = 0;
796
+ this.lastOkAt = this.now();
797
+ if (wasDown) this.notify();
798
+ }
799
+ /** The request failed or the server is overloaded. `retryAfterMs` comes from a `Retry-After` header. */
800
+ failure(retryAfterMs) {
801
+ this.failures += 1;
802
+ const now = this.now();
803
+ if (retryAfterMs != null && retryAfterMs > 0) {
804
+ this.openUntil = Math.max(this.openUntil, now + Math.min(retryAfterMs, MAX_RETRY_AFTER_MS));
805
+ return;
806
+ }
807
+ if (this.failures < this.threshold) return;
808
+ const cooldown = Math.min(this.maxMs, this.baseMs * 2 ** (this.failures - this.threshold));
809
+ this.openUntil = Math.max(this.openUntil, now + cooldown * (0.8 + this.random() * 0.4));
810
+ }
811
+ /** Milliseconds background traffic should wait before trying again (0: send now). */
812
+ waitMs() {
813
+ if (typeof navigator !== "undefined" && navigator.onLine === false) return OFFLINE_WAIT_MS;
814
+ return Math.max(0, this.openUntil - this.now());
815
+ }
816
+ canSend() {
817
+ return this.waitMs() === 0;
818
+ }
819
+ /**
820
+ * The last request got an answer. Unload beacons can't report back, so what they carry is only
821
+ * forgotten while this holds; otherwise it stays queued and is sent again (the server drops duplicates).
822
+ */
823
+ get reachable() {
824
+ return this.failures === 0 && this.lastOkAt > 0;
825
+ }
826
+ /** The browser came back online: end the cooldown and let queued traffic try straight away. */
827
+ online() {
828
+ this.openUntil = 0;
829
+ this.notify();
830
+ }
831
+ /** Called when the server answers again after failures, or the browser comes back online. */
832
+ onRecover(cb) {
833
+ this.listeners.add(cb);
834
+ return () => this.listeners.delete(cb);
835
+ }
836
+ notify() {
837
+ for (const cb of Array.from(this.listeners)) {
838
+ try {
839
+ cb();
840
+ } catch {
841
+ }
842
+ }
843
+ }
844
+ };
845
+ function parseRetryAfter(value, now = Date.now()) {
846
+ if (!value) return null;
847
+ const trimmed = value.trim();
848
+ if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
849
+ const at = Date.parse(trimmed);
850
+ return Number.isNaN(at) ? null : Math.max(0, at - now);
851
+ }
852
+ function isServerFailure(status) {
853
+ return status === 429 || status >= 500;
854
+ }
855
+ var STREAM_RESPONSE_TIMEOUT_MS = 3e4;
856
+ var STREAM_IDLE_TIMEOUT_MS = 6e4;
657
857
  var Api = class {
658
- constructor(baseUrl, key, fetchImpl) {
858
+ constructor(baseUrl, key, fetchImpl, health) {
659
859
  __publicField2(this, "baseUrl", baseUrl);
660
860
  __publicField2(this, "key", key);
661
861
  __publicField2(this, "fetchImpl", fetchImpl);
862
+ __publicField2(this, "health");
863
+ this.health = health ?? new ServerHealth();
864
+ }
865
+ /** Feeds an answer (or a failed request, `null`) into {@link health}. */
866
+ observe(res) {
867
+ if (!res) this.health.failure();
868
+ else if (isServerFailure(res.status)) this.health.failure(parseRetryAfter(res.headers?.get?.("retry-after")));
869
+ else this.health.success();
662
870
  }
663
871
  url(path, params = {}) {
664
872
  const query = Object.entries({ key: this.key, ...params }).filter((e) => e[1] != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
@@ -678,15 +886,22 @@ var Api = class {
678
886
  const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
679
887
  const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
680
888
  try {
681
- const res = await f(this.url(path, options.params), {
682
- method,
683
- headers,
684
- body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
685
- keepalive: options.keepalive,
686
- signal: ac?.signal,
687
- credentials: "omit",
688
- mode: "cors"
689
- });
889
+ let res;
890
+ try {
891
+ res = await f(this.url(path, options.params), {
892
+ method,
893
+ headers,
894
+ body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
895
+ keepalive: options.keepalive,
896
+ signal: ac?.signal,
897
+ credentials: "omit",
898
+ mode: "cors"
899
+ });
900
+ } catch (err) {
901
+ this.observe(null);
902
+ throw err;
903
+ }
904
+ this.observe(res);
690
905
  let data = null;
691
906
  if (res.status !== 304 && res.status !== 204) {
692
907
  try {
@@ -712,30 +927,53 @@ var Api = class {
712
927
  }
713
928
  /**
714
929
  * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
715
- * returned instead. No timeout: Scout replies can take a while and the server keeps the stream alive.
930
+ * returned instead. Scout replies can take a while, so there is no overall timeout: the response
931
+ * must start within 30 s and then never go quiet for more than 60 s (the server sends keepalives),
932
+ * otherwise the request is aborted and rejects.
716
933
  */
717
934
  async stream(path, body, onEvent, headers = {}) {
718
935
  const f = this.fetcher;
719
936
  if (!f) throw new Error("fetch is not available");
720
- const res = await f(this.url(path), {
721
- method: "POST",
722
- headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
723
- body: JSON.stringify(body),
724
- credentials: "omit",
725
- mode: "cors"
726
- });
727
- const type = res.headers?.get?.("content-type") ?? "";
728
- if (!type.includes("text/event-stream") || !res.body) {
729
- let data = null;
937
+ const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
938
+ let timer = null;
939
+ const arm = (ms) => {
940
+ if (!ac) return;
941
+ if (timer) clearTimeout(timer);
942
+ timer = setTimeout(() => ac.abort(), ms);
943
+ };
944
+ arm(STREAM_RESPONSE_TIMEOUT_MS);
945
+ try {
946
+ let res;
730
947
  try {
731
- data = await res.json();
732
- } catch {
733
- data = null;
948
+ res = await f(this.url(path), {
949
+ method: "POST",
950
+ headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
951
+ body: JSON.stringify(body),
952
+ signal: ac?.signal,
953
+ credentials: "omit",
954
+ mode: "cors"
955
+ });
956
+ } catch (err) {
957
+ this.observe(null);
958
+ throw err;
959
+ }
960
+ this.observe(res);
961
+ const type = res.headers?.get?.("content-type") ?? "";
962
+ if (!type.includes("text/event-stream") || !res.body) {
963
+ let data = null;
964
+ try {
965
+ data = await res.json();
966
+ } catch {
967
+ data = null;
968
+ }
969
+ return { status: res.status, ok: res.ok, data, etag: null };
734
970
  }
735
- return { status: res.status, ok: res.ok, data, etag: null };
971
+ arm(STREAM_IDLE_TIMEOUT_MS);
972
+ await readEventStream(res.body, onEvent, () => arm(STREAM_IDLE_TIMEOUT_MS));
973
+ return { status: res.status, ok: res.ok, data: null, etag: null };
974
+ } finally {
975
+ if (timer) clearTimeout(timer);
736
976
  }
737
- await readEventStream(res.body, onEvent);
738
- return { status: res.status, ok: res.ok, data: null, etag: null };
739
977
  }
740
978
  /** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
741
979
  beacon(path, body) {
@@ -766,7 +1004,7 @@ async function withRetry(send, attempts = 3, baseMs = 500) {
766
1004
  function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
767
1005
  return withRetry(() => api.post(path, body), attempts, baseMs);
768
1006
  }
769
- async function readEventStream(body, onEvent) {
1007
+ async function readEventStream(body, onEvent, onChunk) {
770
1008
  const reader = body.getReader();
771
1009
  const decoder = new TextDecoder();
772
1010
  let buffer = "";
@@ -789,6 +1027,7 @@ async function readEventStream(body, onEvent) {
789
1027
  };
790
1028
  for (; ; ) {
791
1029
  const { done, value } = await reader.read();
1030
+ if (value) onChunk?.();
792
1031
  if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
793
1032
  let sep;
794
1033
  while ((sep = buffer.indexOf("\n\n")) >= 0) {
@@ -799,8 +1038,46 @@ async function readEventStream(body, onEvent) {
799
1038
  }
800
1039
  if (buffer.trim()) dispatch(buffer);
801
1040
  }
1041
+ function readJson(store, key, fallback) {
1042
+ const raw = store.get(key);
1043
+ if (raw == null) return fallback;
1044
+ try {
1045
+ return JSON.parse(raw);
1046
+ } catch {
1047
+ return fallback;
1048
+ }
1049
+ }
1050
+ function writeJson(store, key, value) {
1051
+ try {
1052
+ store.set(key, JSON.stringify(value));
1053
+ } catch {
1054
+ }
1055
+ }
1056
+ var KEYS = {
1057
+ anon: "guidely:anon",
1058
+ seen: "guidely:seen",
1059
+ config: (key) => `guidely:config:${key}`,
1060
+ etag: (key) => `guidely:etag:${key}`,
1061
+ state: (key) => `guidely:state:${key}`,
1062
+ user: (key) => `guidely:user:${key}`,
1063
+ queue: (key) => `guidely:queue:${key}`,
1064
+ /** Survey answers and guide/checklist progress not yet accepted by the server. */
1065
+ outbox: (key) => `guidely:outbox:${key}`,
1066
+ /** Agent Analytics turns not yet accepted by the server. */
1067
+ agentOutbox: (key) => `guidely:agents:${key}`,
1068
+ session: "guidely:session",
1069
+ active: "guidely:active",
1070
+ authoring: "guidely:authoring",
1071
+ /** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
1072
+ capture: "guidely:capture",
1073
+ capturePending: "guidely:capture:pending",
1074
+ /** Messenger conversations this browser can read: [{ id, token }]. */
1075
+ conversations: (key) => `guidely:conversations:${key}`,
1076
+ /** Announcement ids opened in the messenger's News tab. */
1077
+ newsSeen: (key) => `guidely:news:${key}`
1078
+ };
802
1079
 
803
- // ../sdk-core/dist/chunk-544KSOC3.js
1080
+ // ../sdk-core/dist/chunk-ZAF2KRJA.js
804
1081
  var EventQueue = class {
805
1082
  constructor(opts) {
806
1083
  __publicField2(this, "opts", opts);
@@ -811,17 +1088,34 @@ var EventQueue = class {
811
1088
  __publicField2(this, "inflightIds", /* @__PURE__ */ new Set());
812
1089
  __publicField2(this, "attempt", 0);
813
1090
  __publicField2(this, "destroyed", false);
1091
+ __publicField2(this, "offRecover", null);
814
1092
  __publicField2(this, "flushIntervalMs");
815
1093
  __publicField2(this, "maxQueue");
816
1094
  __publicField2(this, "batchSize");
817
1095
  __publicField2(this, "maxAttempts");
818
1096
  this.flushIntervalMs = opts.flushIntervalMs ?? 2e3;
819
- this.maxQueue = opts.maxQueue ?? 200;
1097
+ this.maxQueue = opts.maxQueue ?? 500;
820
1098
  this.batchSize = opts.batchSize ?? 50;
821
1099
  this.maxAttempts = opts.maxAttempts ?? 5;
822
1100
  const persisted = readJson(opts.store, opts.storageKey, []);
823
1101
  if (Array.isArray(persisted)) this.events = persisted.filter((e) => e && typeof e.eventId === "string").slice(-this.maxQueue);
824
1102
  if (this.events.length) this.schedule();
1103
+ this.offRecover = opts.health?.onRecover(() => this.retryNow()) ?? null;
1104
+ }
1105
+ /** The API is back: drop any pending backoff and send what is queued. */
1106
+ retryNow() {
1107
+ if (this.destroyed) return;
1108
+ if (this.retryTimer) clearTimeout(this.retryTimer);
1109
+ this.retryTimer = null;
1110
+ this.attempt = 0;
1111
+ void this.flush();
1112
+ }
1113
+ retryIn(ms) {
1114
+ if (this.retryTimer || this.destroyed) return;
1115
+ this.retryTimer = setTimeout(() => {
1116
+ this.retryTimer = null;
1117
+ void this.flush();
1118
+ }, ms);
825
1119
  }
826
1120
  get size() {
827
1121
  return this.events.length;
@@ -855,6 +1149,8 @@ var EventQueue = class {
855
1149
  clearTimeout(this.timer);
856
1150
  this.timer = null;
857
1151
  }
1152
+ const wait = this.opts.health?.waitMs() ?? 0;
1153
+ if (wait > 0) return this.retryIn(wait);
858
1154
  this.inflight = true;
859
1155
  const batch = this.events.slice(0, this.batchSize);
860
1156
  this.inflightIds = new Set(batch.map((e) => e.eventId));
@@ -878,26 +1174,20 @@ var EventQueue = class {
878
1174
  if (this.events.length) this.schedule();
879
1175
  return;
880
1176
  }
881
- this.attempt += 1;
882
- if (this.attempt >= this.maxAttempts) {
883
- this.attempt = 0;
884
- return;
885
- }
886
- this.retryTimer = setTimeout(() => {
887
- this.retryTimer = null;
888
- void this.flush();
889
- }, backoffMs(this.attempt - 1));
1177
+ this.attempt = Math.min(this.attempt + 1, this.maxAttempts);
1178
+ const delay = this.attempt >= this.maxAttempts ? 6e4 : backoffMs(this.attempt - 1);
1179
+ this.retryIn(Math.max(delay, this.opts.health?.waitMs() ?? 0));
890
1180
  }
891
1181
  /** Best-effort synchronous flush on unload using `sendBeacon`. */
892
1182
  flushSync() {
893
1183
  if (this.destroyed || !this.opts.enabled() || !this.opts.beacon) return;
894
1184
  const batch = this.events.filter((e) => !this.inflightIds.has(e.eventId)).slice(0, this.batchSize);
895
1185
  if (!batch.length) return;
896
- if (this.opts.beacon(batch)) {
1186
+ if (this.opts.beacon(batch) && (this.opts.health?.reachable ?? true)) {
897
1187
  const ids = new Set(batch.map((e) => e.eventId));
898
1188
  this.events = this.events.filter((e) => !ids.has(e.eventId));
899
- this.persist();
900
1189
  }
1190
+ this.persist();
901
1191
  }
902
1192
  clear() {
903
1193
  this.events = [];
@@ -905,6 +1195,7 @@ var EventQueue = class {
905
1195
  }
906
1196
  destroy() {
907
1197
  this.destroyed = true;
1198
+ this.offRecover?.();
908
1199
  if (this.timer) clearTimeout(this.timer);
909
1200
  if (this.retryTimer) clearTimeout(this.retryTimer);
910
1201
  this.timer = null;
@@ -912,6 +1203,88 @@ var EventQueue = class {
912
1203
  this.persist();
913
1204
  }
914
1205
  };
1206
+ var Outbox = class {
1207
+ constructor(opts) {
1208
+ __publicField2(this, "opts", opts);
1209
+ __publicField2(this, "items");
1210
+ __publicField2(this, "sending", false);
1211
+ __publicField2(this, "attempt", 0);
1212
+ __publicField2(this, "timer", null);
1213
+ __publicField2(this, "destroyed", false);
1214
+ __publicField2(this, "offRecover");
1215
+ __publicField2(this, "maxItems");
1216
+ __publicField2(this, "maxAgeMs");
1217
+ this.maxItems = opts.maxItems ?? 50;
1218
+ this.maxAgeMs = opts.maxAgeMs ?? 7 * 24 * 60 * 60 * 1e3;
1219
+ const stored = readJson(opts.store, opts.storageKey, []);
1220
+ const cutoff = Date.now() - this.maxAgeMs;
1221
+ this.items = Array.isArray(stored) ? stored.filter((i) => i && typeof i.path === "string" && typeof i.key === "string" && i.createdAt > cutoff).slice(-this.maxItems) : [];
1222
+ this.offRecover = opts.health?.onRecover(() => {
1223
+ this.attempt = 0;
1224
+ this.retryIn(0);
1225
+ }) ?? null;
1226
+ if (this.items.length) this.retryIn(0);
1227
+ }
1228
+ get size() {
1229
+ return this.items.length;
1230
+ }
1231
+ /** Queues `POST path` and sends it now (or as soon as the API is reachable). */
1232
+ add(key, path, body) {
1233
+ if (this.destroyed) return;
1234
+ this.items = this.items.filter((i) => i.key !== key);
1235
+ this.items.push({ key, path, body, createdAt: Date.now() });
1236
+ if (this.items.length > this.maxItems) this.items.splice(0, this.items.length - this.maxItems);
1237
+ this.persist();
1238
+ void this.flush();
1239
+ }
1240
+ persist() {
1241
+ writeJson(this.opts.store, this.opts.storageKey, this.items);
1242
+ }
1243
+ retryIn(ms) {
1244
+ if (this.destroyed) return;
1245
+ if (this.timer) clearTimeout(this.timer);
1246
+ this.timer = setTimeout(() => {
1247
+ this.timer = null;
1248
+ void this.flush();
1249
+ }, ms);
1250
+ }
1251
+ /** Sends queued requests in order, one at a time. */
1252
+ async flush() {
1253
+ if (this.destroyed || this.sending || !this.items.length) return;
1254
+ const wait = this.opts.health?.waitMs() ?? 0;
1255
+ if (wait > 0) return this.retryIn(wait);
1256
+ this.sending = true;
1257
+ try {
1258
+ while (this.items.length && !this.destroyed) {
1259
+ const item = this.items[0];
1260
+ let retry = false;
1261
+ try {
1262
+ const res = await this.opts.api.post(item.path, item.body);
1263
+ retry = !res.ok && (res.status === 429 || res.status >= 500);
1264
+ } catch (err) {
1265
+ retry = true;
1266
+ this.opts.onError?.(err);
1267
+ }
1268
+ if (retry) {
1269
+ this.attempt += 1;
1270
+ this.retryIn(Math.max(backoffMs(this.attempt - 1, 1e3, 6e4), this.opts.health?.waitMs() ?? 0));
1271
+ return;
1272
+ }
1273
+ this.attempt = 0;
1274
+ this.items = this.items.filter((i) => i !== item);
1275
+ this.persist();
1276
+ }
1277
+ } finally {
1278
+ this.sending = false;
1279
+ }
1280
+ }
1281
+ destroy() {
1282
+ this.destroyed = true;
1283
+ this.offRecover?.();
1284
+ if (this.timer) clearTimeout(this.timer);
1285
+ this.timer = null;
1286
+ }
1287
+ };
915
1288
  var Emitter = class {
916
1289
  constructor(onError) {
917
1290
  __publicField2(this, "onError", onError);
@@ -980,12 +1353,32 @@ function areMutuallyExclusive(a, b) {
980
1353
  return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
981
1354
  }
982
1355
  function isWithinSchedule(guide, now = Date.now()) {
983
- const s = guide.config.survey;
984
- if (!s) return true;
985
- if (s.startAt && now < Date.parse(s.startAt)) return false;
986
- if (s.endAt && now > Date.parse(s.endAt)) return false;
1356
+ for (const w of [guide.config.schedule, guide.config.survey]) {
1357
+ if (!w) continue;
1358
+ if (w.startAt && now < Date.parse(w.startAt)) return false;
1359
+ if (w.endAt && now > Date.parse(w.endAt)) return false;
1360
+ }
1361
+ return true;
1362
+ }
1363
+ var MINUTE = 60 * 1e3;
1364
+ function countsTowardThrottle(guide) {
1365
+ return isExclusiveGuide(guide) && !guide.config.frequency.ignoreThrottling;
1366
+ }
1367
+ function isThrottleEligible(settings, launches, now = Date.now()) {
1368
+ if (!settings || !settings.enabled) return true;
1369
+ const times = launches.map((l) => Date.parse(l)).filter((t) => !Number.isNaN(t));
1370
+ const windowMs = Math.max(1, settings.windowHours) * HOUR;
1371
+ if (times.filter((t) => now - t < windowMs).length >= Math.max(1, settings.maxGuides)) return false;
1372
+ if (settings.minGapMinutes > 0 && times.some((t) => now - t < settings.minGapMinutes * MINUTE)) return false;
987
1373
  return true;
988
1374
  }
1375
+ function pruneLaunches(launches, settings, now = Date.now()) {
1376
+ const keepMs = Math.max(settings?.windowHours ?? 24, 1) * HOUR + (settings?.minGapMinutes ?? 0) * MINUTE;
1377
+ return launches.filter((l) => {
1378
+ const t = Date.parse(l);
1379
+ return !Number.isNaN(t) && now - t < keepMs;
1380
+ }).slice(-100);
1381
+ }
989
1382
  function dismissedAfterMerge(local, server) {
990
1383
  if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
991
1384
  return local?.dismissedAt ?? server.dismissedAt ?? null;
@@ -994,6 +1387,115 @@ function dismissedAfterMerge(local, server) {
994
1387
  // src/core/client.ts
995
1388
  var import_flags2 = require("@guidelytech/flags");
996
1389
 
1390
+ // src/core/autocapture.ts
1391
+ var TAP_SLOP = 10;
1392
+ var TAP_MAX_MS = 1e3;
1393
+ var HEATMAP_MAX_POINTS = 40;
1394
+ function quoteValue(value) {
1395
+ return `"${value.replace(/["\\]/g, "\\$&")}"`;
1396
+ }
1397
+ function attrSelector(name, value) {
1398
+ return `[${name}=${quoteValue(value)}]`.slice(0, 300);
1399
+ }
1400
+ function cleanText(text) {
1401
+ const t = (text ?? "").replace(/\s+/g, " ").trim();
1402
+ return t ? t.slice(0, 100) : void 0;
1403
+ }
1404
+ function tapProperties(el, x, y, platform, maskAll) {
1405
+ const props = { selector: el.selector.slice(0, 300), tag: el.tag || "View", x: Math.round(x) || 0, y: Math.round(y) || 0, platform };
1406
+ const text = maskAll ? void 0 : cleanText(el.text);
1407
+ if (text) props.text = text;
1408
+ return props;
1409
+ }
1410
+ var r2 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 100) / 100;
1411
+ function heatmapPoint(x, y, el, rect, rage) {
1412
+ const p = { k: rage ? "r" : "c", x: Math.round(x) || 0, y: Math.round(y) || 0 };
1413
+ const s = el?.selector.slice(0, 120);
1414
+ if (s) p.s = s;
1415
+ if (s && rect && rect.width > 0 && rect.height > 0) {
1416
+ p.rx = r2((x - rect.x) / rect.width);
1417
+ p.ry = r2((y - rect.y) / rect.height);
1418
+ }
1419
+ return p;
1420
+ }
1421
+ var RageClickDetector = class {
1422
+ constructor(threshold = 3, windowMs = 1e3, radius = 30) {
1423
+ __publicField(this, "threshold", threshold);
1424
+ __publicField(this, "windowMs", windowMs);
1425
+ __publicField(this, "radius", radius);
1426
+ __publicField(this, "clicks", []);
1427
+ __publicField(this, "fired", false);
1428
+ }
1429
+ /** Taps in the current burst once it has fired (rage), otherwise 0. */
1430
+ get burst() {
1431
+ return this.fired ? this.clicks.length : 0;
1432
+ }
1433
+ /** Records a tap; returns the tap count when it completes a burst, otherwise 0. */
1434
+ add(x, y, t) {
1435
+ const first = this.clicks[0];
1436
+ const last = this.clicks[this.clicks.length - 1];
1437
+ if (!first || !last || t - last.t > this.windowMs || Math.hypot(x - first.x, y - first.y) > this.radius) {
1438
+ this.clicks = [];
1439
+ this.fired = false;
1440
+ }
1441
+ this.clicks = this.clicks.filter((c) => t - c.t <= this.windowMs);
1442
+ this.clicks.push({ x, y, t });
1443
+ if (this.fired || this.clicks.length < this.threshold) return 0;
1444
+ this.fired = true;
1445
+ return this.clicks.length;
1446
+ }
1447
+ };
1448
+ var HeatmapBuffer = class {
1449
+ constructor() {
1450
+ /** Screen path the buffered points belong to. */
1451
+ __publicField(this, "url", "");
1452
+ __publicField(this, "points", []);
1453
+ }
1454
+ /** Adds a tap (`burst` > 0 marks it and the burst's previous taps as rage); true when the batch is full. */
1455
+ add(url, point, burst) {
1456
+ this.url = url;
1457
+ for (let i = Math.max(0, this.points.length - burst + 1); i < this.points.length; i++) this.points[i].k = "r";
1458
+ this.points.push(point);
1459
+ return this.points.length >= HEATMAP_MAX_POINTS;
1460
+ }
1461
+ /** Empties the buffer, returning what was in it. */
1462
+ take() {
1463
+ const points = this.points;
1464
+ this.points = [];
1465
+ return points.length ? { url: this.url, points } : null;
1466
+ }
1467
+ };
1468
+ var TapTracker = class {
1469
+ constructor() {
1470
+ __publicField(this, "start", null);
1471
+ __publicField(this, "moved", 0);
1472
+ }
1473
+ /** A finger went down. A second finger while one is tracked cancels the gesture (not a tap). */
1474
+ begin(x, y, t, touches, id) {
1475
+ if (this.start || touches > 1) {
1476
+ this.start = null;
1477
+ return;
1478
+ }
1479
+ this.start = { x, y, t, id };
1480
+ this.moved = 0;
1481
+ }
1482
+ move(x, y, id) {
1483
+ const s = this.start;
1484
+ if (s && (id === void 0 || s.id === void 0 || id === s.id)) this.moved = Math.max(this.moved, Math.hypot(x - s.x, y - s.y));
1485
+ }
1486
+ /** The finger lifted: the tap position when it moved ≤ 10pt and lasted < 1s, else null. */
1487
+ end(x, y, t, id) {
1488
+ const s = this.start;
1489
+ if (!s || id !== void 0 && s.id !== void 0 && id !== s.id) return null;
1490
+ this.start = null;
1491
+ const moved = Math.max(this.moved, Math.hypot(x - s.x, y - s.y));
1492
+ return moved <= TAP_SLOP && t - s.t < TAP_MAX_MS ? { x, y } : null;
1493
+ }
1494
+ cancel() {
1495
+ this.start = null;
1496
+ }
1497
+ };
1498
+
997
1499
  // src/core/targets.ts
998
1500
  var TargetTimeoutError = class extends Error {
999
1501
  constructor(ids) {
@@ -1096,6 +1598,35 @@ var TargetRegistry = class {
1096
1598
  return null;
1097
1599
  }
1098
1600
  }
1601
+ /**
1602
+ * The registered target on the current screen whose visible frame contains the point (window
1603
+ * coordinates); the smallest one when targets are nested. Frames that do not answer within
1604
+ * `timeoutMs` are skipped.
1605
+ */
1606
+ async hitTest(x, y, timeoutMs = 500) {
1607
+ const candidates = [];
1608
+ this.targets.forEach((list, id) => {
1609
+ for (const handle of list) if (!(handle.screen && this.screen && handle.screen !== this.screen)) candidates.push({ id, handle });
1610
+ });
1611
+ if (!candidates.length) return null;
1612
+ const rects = await Promise.all(
1613
+ candidates.map(
1614
+ ({ handle }) => new Promise((resolve) => {
1615
+ const timer = setTimeout(() => resolve(null), timeoutMs);
1616
+ void this.measureHandle(handle).then((r) => {
1617
+ clearTimeout(timer);
1618
+ resolve(r);
1619
+ });
1620
+ })
1621
+ )
1622
+ );
1623
+ let best = null;
1624
+ rects.forEach((r, i) => {
1625
+ if (!r || x < r.x || y < r.y || x > r.x + r.width || y > r.y + r.height) return;
1626
+ if (!best || r.width * r.height < best.rect.width * best.rect.height) best = { id: candidates[i].id, rect: r };
1627
+ });
1628
+ return best;
1629
+ }
1099
1630
  /** First of `ids` that is mounted and visible. */
1100
1631
  async resolve(ids) {
1101
1632
  for (const id of ids) {
@@ -1467,12 +1998,29 @@ function createSseParser(onEvent) {
1467
1998
  }
1468
1999
  };
1469
2000
  }
2001
+ var STREAM_RESPONSE_TIMEOUT_MS2 = 3e4;
2002
+ var STREAM_IDLE_TIMEOUT_MS2 = 6e4;
1470
2003
  function xhrStreamPost() {
1471
2004
  if (typeof XMLHttpRequest === "undefined") return null;
1472
2005
  return (url, headers, body, onChunk) => new Promise((resolve, reject) => {
1473
2006
  const xhr = new XMLHttpRequest();
1474
2007
  let seen = 0;
1475
2008
  let streamed = false;
2009
+ let timer = null;
2010
+ let settled = false;
2011
+ const settle = (fn) => {
2012
+ if (settled) return;
2013
+ settled = true;
2014
+ if (timer) clearTimeout(timer);
2015
+ fn();
2016
+ };
2017
+ const arm = (ms, what) => {
2018
+ if (timer) clearTimeout(timer);
2019
+ timer = setTimeout(() => {
2020
+ settle(() => reject(new Error(`Scout ${what} timed out`)));
2021
+ xhr.abort();
2022
+ }, ms);
2023
+ };
1476
2024
  const pump = () => {
1477
2025
  const type = xhr.getResponseHeader("content-type") || "";
1478
2026
  if (!type.includes("text/event-stream")) return;
@@ -1485,8 +2033,15 @@ function xhrStreamPost() {
1485
2033
  };
1486
2034
  xhr.open("POST", url);
1487
2035
  for (const [k, v] of Object.entries(headers)) xhr.setRequestHeader(k, v);
1488
- xhr.onprogress = pump;
1489
- xhr.onload = () => {
2036
+ xhr.onreadystatechange = () => {
2037
+ if (!settled && xhr.readyState >= 2 && xhr.readyState < 4) arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
2038
+ };
2039
+ xhr.onprogress = () => {
2040
+ if (settled) return;
2041
+ arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
2042
+ pump();
2043
+ };
2044
+ xhr.onload = () => settle(() => {
1490
2045
  pump();
1491
2046
  let json = null;
1492
2047
  if (!streamed) {
@@ -1497,27 +2052,74 @@ function xhrStreamPost() {
1497
2052
  }
1498
2053
  }
1499
2054
  resolve({ status: xhr.status, json, streamed });
1500
- };
1501
- xhr.onerror = () => reject(new Error("Network request failed"));
2055
+ });
2056
+ xhr.onerror = () => settle(() => reject(new Error("Network request failed")));
2057
+ arm(STREAM_RESPONSE_TIMEOUT_MS2, "request");
1502
2058
  xhr.send(JSON.stringify(body));
1503
2059
  });
1504
2060
  }
2061
+ function bodyReader(res) {
2062
+ const body = res.body;
2063
+ const Decoder = globalThis.TextDecoder;
2064
+ return body && typeof body.getReader === "function" && Decoder ? { reader: body.getReader(), decoder: new Decoder() } : null;
2065
+ }
1505
2066
  function fetchStreamPost(fetchImpl) {
1506
2067
  return async (url, headers, body, onChunk) => {
1507
- const res = await fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body) });
1508
- const type = res.headers.get("content-type") || "";
1509
- const text = await res.text();
1510
- if (type.includes("text/event-stream")) {
1511
- onChunk(text);
1512
- return { status: res.status, json: null, streamed: true };
1513
- }
1514
- let json = null;
2068
+ const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
2069
+ let timer = null;
2070
+ let timedOut = "";
2071
+ let fail = () => {
2072
+ };
2073
+ const expired = new Promise((_, reject) => fail = reject);
2074
+ expired.catch(() => {
2075
+ });
2076
+ const arm = (ms, what) => {
2077
+ if (timer) clearTimeout(timer);
2078
+ timer = setTimeout(() => {
2079
+ timedOut = what;
2080
+ ac?.abort();
2081
+ fail(new Error(`Scout ${what} timed out`));
2082
+ }, ms);
2083
+ };
2084
+ const race = (p) => Promise.race([p, expired]);
2085
+ arm(STREAM_RESPONSE_TIMEOUT_MS2, "request");
1515
2086
  try {
1516
- json = JSON.parse(text || "null");
1517
- } catch {
1518
- json = null;
2087
+ const res = await race(fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body), signal: ac?.signal }));
2088
+ arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
2089
+ const type = res.headers.get("content-type") || "";
2090
+ const sse = type.includes("text/event-stream");
2091
+ const streaming = sse ? bodyReader(res) : null;
2092
+ if (streaming) {
2093
+ const { reader, decoder } = streaming;
2094
+ for (; ; ) {
2095
+ const { done, value } = await race(reader.read());
2096
+ if (value) {
2097
+ arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
2098
+ onChunk(decoder.decode(value, { stream: true }));
2099
+ }
2100
+ if (done) break;
2101
+ }
2102
+ const rest = decoder.decode();
2103
+ if (rest) onChunk(rest);
2104
+ return { status: res.status, json: null, streamed: true };
2105
+ }
2106
+ const text = await race(res.text());
2107
+ if (sse) {
2108
+ onChunk(text);
2109
+ return { status: res.status, json: null, streamed: true };
2110
+ }
2111
+ let json = null;
2112
+ try {
2113
+ json = JSON.parse(text || "null");
2114
+ } catch {
2115
+ json = null;
2116
+ }
2117
+ return { status: res.status, json, streamed: false };
2118
+ } catch (err) {
2119
+ throw timedOut ? new Error(`Scout ${timedOut} timed out`) : err;
2120
+ } finally {
2121
+ if (timer) clearTimeout(timer);
1519
2122
  }
1520
- return { status: res.status, json, streamed: false };
1521
2123
  };
1522
2124
  }
1523
2125
  var byNewest = (a, b) => Date.parse(b.lastMessageAt) - Date.parse(a.lastMessageAt);
@@ -1702,7 +2304,15 @@ var Messenger = class {
1702
2304
  }
1703
2305
  });
1704
2306
  try {
1705
- const res = await this.deps.streamPost(this.url(path), headers, payload, (chunk) => parser.feed(chunk));
2307
+ let res;
2308
+ try {
2309
+ res = await this.deps.streamPost(this.url(path), headers, payload, (chunk) => parser.feed(chunk));
2310
+ } catch (err) {
2311
+ this.deps.health?.failure();
2312
+ throw err;
2313
+ }
2314
+ if (isServerFailure(res.status)) this.deps.health?.failure();
2315
+ else this.deps.health?.success();
1706
2316
  parser.end();
1707
2317
  if (res.status < 200 || res.status >= 300) throw new Error(`Request failed (${res.status})`);
1708
2318
  if (!res.streamed && res.json) {
@@ -1774,7 +2384,8 @@ var Messenger = class {
1774
2384
  this.stopThreadPolling();
1775
2385
  this.set({ open: true, view: { kind: "article", articleId: id, from }, article: null });
1776
2386
  try {
1777
- const res = await this.request("GET", `/articles/${encodeURIComponent(id)}`);
2387
+ const locale = this.deps.locale?.();
2388
+ const res = await this.request("GET", `/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : {});
1778
2389
  this.set({ article: res.article });
1779
2390
  } catch (err) {
1780
2391
  this.deps.onError(err);
@@ -1889,9 +2500,22 @@ var PersistentStore = class {
1889
2500
 
1890
2501
  // src/core/client.ts
1891
2502
  var SDK_NAME = "@guidelytech/react-native";
1892
- var SDK_VERSION = "0.9.0";
2503
+ var SDK_VERSION = "0.11.0";
1893
2504
  var DEFAULT_API_URL = "https://guidelytech.com";
1894
2505
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
2506
+ var CACHED_START_MS = 1500;
2507
+ function settlesWithin(promise, ms) {
2508
+ return new Promise((resolve) => {
2509
+ const timer = setTimeout(() => resolve(false), ms);
2510
+ const done = () => {
2511
+ clearTimeout(timer);
2512
+ resolve(true);
2513
+ };
2514
+ promise.then(done, done);
2515
+ });
2516
+ }
2517
+ var appVersionKey = (key) => `guidely:app:${key}`;
2518
+ var UNKNOWN_VIEW = { selector: "View", tag: "View" };
1895
2519
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
1896
2520
  var emptyState = () => ({ guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] });
1897
2521
  var newSession = () => ({ id: `s_${randomId(16)}`, startedAt: nowIso(), lastActiveAt: nowIso(), shownGuides: [], events: [] });
@@ -1904,6 +2528,8 @@ var GuidelyClient = class {
1904
2528
  __publicField(this, "store");
1905
2529
  __publicField(this, "emitter");
1906
2530
  __publicField(this, "queue", null);
2531
+ /** Survey answers, guide completions/dismissals and checklist progress, kept until the server has them. */
2532
+ __publicField(this, "outbox", null);
1907
2533
  __publicField(this, "config", null);
1908
2534
  __publicField(this, "configFetchedAt", 0);
1909
2535
  __publicField(this, "etag", null);
@@ -1921,6 +2547,7 @@ var GuidelyClient = class {
1921
2547
  __publicField(this, "activeSurvey", null);
1922
2548
  __publicField(this, "queued", []);
1923
2549
  __publicField(this, "flows", /* @__PURE__ */ new Map());
2550
+ /** Checklists on screen, with the guide as rendered (in the visitor's language). */
1924
2551
  __publicField(this, "checklists", /* @__PURE__ */ new Map());
1925
2552
  __publicField(this, "expanded", /* @__PURE__ */ new Set());
1926
2553
  __publicField(this, "surveyView", null);
@@ -1940,6 +2567,27 @@ var GuidelyClient = class {
1940
2567
  __publicField(this, "flags");
1941
2568
  /** Experiments already reported this session (`experiment_viewed` is sent once per session). */
1942
2569
  __publicField(this, "exposures", /* @__PURE__ */ new Set());
2570
+ // Product analytics (autocapture).
2571
+ __publicField(this, "rage", new RageClickDetector());
2572
+ __publicField(this, "heatmap", new HeatmapBuffer());
2573
+ /** Taps are processed in order (target frames are measured asynchronously). */
2574
+ __publicField(this, "tapChain", Promise.resolve());
2575
+ /** When the current screen's foreground measurement started; `screenLeft` once it has been reported. */
2576
+ __publicField(this, "screenStartedAt", 0);
2577
+ __publicField(this, "screenLeft", true);
2578
+ /** Last `app_open` (cold start or return to the foreground), for `app_background.foregroundMs`. */
2579
+ __publicField(this, "openedAt", Date.now());
2580
+ __publicField(this, "launchChecked", false);
2581
+ /** An identity was stored before this launch (an existing install adopting lifecycle events). */
2582
+ __publicField(this, "hadIdentity", false);
2583
+ /** Language set with `setLocale()` (or the `locale` option). */
2584
+ __publicField(this, "locale", null);
2585
+ /** Language each shown guide was rendered in; added to its events as `locale`. */
2586
+ __publicField(this, "guideLocales", /* @__PURE__ */ new Map());
2587
+ /** The resource center in the visitor's language, recomputed when the config or language changes. */
2588
+ __publicField(this, "localizedRc", null);
2589
+ /** "What's new" last-seen time when the resource center opened (entries after it read as new). */
2590
+ __publicField(this, "announcementsSeenAtOpen", null);
1943
2591
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
1944
2592
  __publicField(this, "eventBus", {
1945
2593
  on: (name, cb) => {
@@ -1966,9 +2614,10 @@ var GuidelyClient = class {
1966
2614
  this.emitter = new Emitter((err) => this.reportError(err));
1967
2615
  this.api = new Api(this.options.apiUrl, this.options.projectId, options.fetch);
1968
2616
  this.store = new PersistentStore(options.storage ?? null, (err) => this.log("storage error", err));
1969
- if (!options.storage) this.log("no `storage` passed: identity and guide history last only for this app session. Pass AsyncStorage to persist them.");
2617
+ if (!options.storage) this.log("no `storage` passed: identity, guide history, queued events and survey answers last only for this app session, so whatever could not be sent during a Guidely outage is lost when the app restarts. Pass AsyncStorage to persist them.");
1970
2618
  this.cleanups.push(this.targets.onTap((id) => this.handleTap(id)));
1971
2619
  if (options.user) this.user = { ...options.user };
2620
+ this.locale = options.locale ? normalizeLocale(options.locale) || null : null;
1972
2621
  this.flags = this.createFlags(options.flags);
1973
2622
  }
1974
2623
  createFlags(opts) {
@@ -2017,9 +2666,10 @@ var GuidelyClient = class {
2017
2666
  async bootstrap() {
2018
2667
  const key = this.options.projectId;
2019
2668
  try {
2020
- await this.store.hydrate([KEYS.anon, KEYS.seen, KEYS.session, KEYS.config(key), KEYS.etag(key), KEYS.state(key), KEYS.user(key), KEYS.queue(key), KEYS.conversations(key)]);
2669
+ await this.store.hydrate([KEYS.anon, KEYS.seen, KEYS.session, KEYS.config(key), KEYS.etag(key), KEYS.state(key), KEYS.user(key), KEYS.queue(key), KEYS.outbox(key), KEYS.conversations(key), appVersionKey(key)]);
2021
2670
  if (this.destroyed) return;
2022
2671
  this.anonymousId = this.store.get(KEYS.anon) || "";
2672
+ this.hadIdentity = !!this.anonymousId;
2023
2673
  if (!this.anonymousId) {
2024
2674
  this.anonymousId = `anon_${randomId(20)}`;
2025
2675
  this.store.set(KEYS.anon, this.anonymousId);
@@ -2037,22 +2687,28 @@ var GuidelyClient = class {
2037
2687
  store: this.store,
2038
2688
  storageKey: KEYS.queue(key),
2039
2689
  enabled: () => this.analyticsEnabled(),
2690
+ health: this.api.health,
2040
2691
  send: async (events) => {
2041
2692
  const res = await this.api.post("/events", { events, sentAt: nowIso(), sdk: { name: SDK_NAME, version: SDK_VERSION } });
2042
2693
  return { ok: res.ok, retry: !(res.status >= 400 && res.status < 500 && res.status !== 429) };
2043
2694
  },
2044
2695
  onError: (err) => this.reportError(err)
2045
2696
  });
2697
+ this.outbox = new Outbox({ api: this.api, store: this.store, storageKey: KEYS.outbox(key), health: this.api.health });
2046
2698
  this.hydrated = true;
2047
2699
  const cached = readJson(this.store, KEYS.config(key), null);
2048
2700
  this.etag = this.store.get(KEYS.etag(key));
2049
2701
  if (cached && Array.isArray(cached.guides)) this.config = forPlatform(cached, this.bridge.device.platform);
2050
2702
  this.syncMessenger();
2051
2703
  this.pending.splice(0).forEach((fn) => fn());
2052
- await this.fetchConfig();
2704
+ const before = this.config?.configVersion;
2705
+ const fresh = this.fetchConfig();
2706
+ if (!this.config) await fresh;
2707
+ else if (!await settlesWithin(fresh, CACHED_START_MS)) void fresh.then(() => this.applyConfigChange(before));
2053
2708
  if (this.destroyed) return;
2709
+ this.checkLaunch();
2054
2710
  this.wireTimers();
2055
- if (this.user?.id) await this.identify(this.user.id, this.user.traits, this.user.account);
2711
+ if (this.user?.id) await settlesWithin(this.identify(this.user.id, this.user.traits, this.user.account), CACHED_START_MS);
2056
2712
  if (this.destroyed) return;
2057
2713
  this.isReady = true;
2058
2714
  if (this.config) this.emitter.emit("ready", this.config);
@@ -2080,6 +2736,7 @@ var GuidelyClient = class {
2080
2736
  if (res.etag) this.store.set(KEYS.etag(this.options.projectId), res.etag);
2081
2737
  else this.store.remove(KEYS.etag(this.options.projectId));
2082
2738
  this.syncMessenger();
2739
+ if (this.hydrated) this.checkLaunch();
2083
2740
  this.emitter.emit("config", this.config);
2084
2741
  this.log("config loaded", this.config.configVersion, `${this.config.guides.length} guides`);
2085
2742
  return true;
@@ -2092,13 +2749,15 @@ var GuidelyClient = class {
2092
2749
  }
2093
2750
  }
2094
2751
  wireTimers() {
2095
- const refresh = setInterval(() => void this.refresh(), this.options.configRefreshMs);
2752
+ const refresh = setInterval(() => {
2753
+ if (this.api.health.canSend()) void this.refresh();
2754
+ }, this.options.configRefreshMs);
2096
2755
  this.cleanups.push(() => clearInterval(refresh));
2097
2756
  const poll = setInterval(() => {
2098
2757
  if (!this.isReady || !this.config || this.backgroundedAt !== null) return;
2099
2758
  const url = this.currentUrl();
2100
2759
  const ctx = this.getContext();
2101
- for (const guide of this.config.guides) if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
2760
+ for (const guide of this.candidates()) if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
2102
2761
  this.drainQueue(url, ctx);
2103
2762
  }, 1e3);
2104
2763
  this.cleanups.push(() => clearInterval(poll));
@@ -2108,6 +2767,11 @@ var GuidelyClient = class {
2108
2767
  if (this.destroyed) return;
2109
2768
  const before = this.config?.configVersion;
2110
2769
  await this.fetchConfig();
2770
+ this.applyConfigChange(before);
2771
+ }
2772
+ /** Re-renders after a config change (refresh, or a config that arrived after start). */
2773
+ applyConfigChange(before) {
2774
+ if (this.destroyed || !this.isReady) return;
2111
2775
  if (this.config && this.config.configVersion !== before) {
2112
2776
  this.notify();
2113
2777
  this.evaluate("ready");
@@ -2117,10 +2781,13 @@ var GuidelyClient = class {
2117
2781
  setAppState(state) {
2118
2782
  if (this.destroyed || !this.hydrated) return;
2119
2783
  if (state === "background") {
2784
+ const wasForeground = this.backgroundedAt === null;
2120
2785
  this.backgroundedAt = Date.now();
2786
+ if (wasForeground) this.wentToBackground();
2121
2787
  this.session.lastActiveAt = nowIso();
2122
2788
  this.persistSession();
2123
2789
  void this.queue?.flush();
2790
+ void this.outbox?.flush();
2124
2791
  void this.store.flush();
2125
2792
  void this.flags.flush();
2126
2793
  this.messenger?.setForeground(false);
@@ -2133,8 +2800,9 @@ var GuidelyClient = class {
2133
2800
  this.persistSession();
2134
2801
  this.firedDelays.clear();
2135
2802
  }
2803
+ this.cameToForeground();
2136
2804
  void this.flags.refresh();
2137
- if (Date.now() - this.configFetchedAt > this.options.configRefreshMs) void this.refresh();
2805
+ if (Date.now() - this.configFetchedAt > this.options.configRefreshMs && this.api.health.canSend()) void this.refresh();
2138
2806
  else if (this.isReady) this.evaluate("ready");
2139
2807
  }
2140
2808
  }
@@ -2145,6 +2813,7 @@ var GuidelyClient = class {
2145
2813
  /** Tears everything down. Create a new client to start again. */
2146
2814
  destroy() {
2147
2815
  if (this.destroyed) return;
2816
+ this.flushHeatmap();
2148
2817
  this.destroyed = true;
2149
2818
  this.active?.controller.destroy();
2150
2819
  this.active = null;
@@ -2160,6 +2829,7 @@ var GuidelyClient = class {
2160
2829
  this.cleanups.splice(0).forEach((fn) => fn());
2161
2830
  void this.queue?.flush();
2162
2831
  this.queue?.destroy();
2832
+ this.outbox?.destroy();
2163
2833
  this.flags.destroy();
2164
2834
  this.emitter.clear();
2165
2835
  this.uiListeners.clear();
@@ -2195,6 +2865,7 @@ var GuidelyClient = class {
2195
2865
  for (const [id, s] of Object.entries(data.guideState || {})) {
2196
2866
  const local = guides[id];
2197
2867
  guides[id] = {
2868
+ ...local,
2198
2869
  impressions: Math.max(local?.impressions ?? 0, s.impressions ?? 0),
2199
2870
  completedAt: local?.completedAt ?? s.completedAt ?? null,
2200
2871
  dismissedAt: local?.dismissedAt ?? s.dismissedAt ?? null,
@@ -2216,7 +2887,10 @@ var GuidelyClient = class {
2216
2887
  checklists,
2217
2888
  surveysSubmitted: Array.from(/* @__PURE__ */ new Set([...this.state.surveysSubmitted, ...data.surveysSubmitted || []])),
2218
2889
  audienceIds: data.audienceIds || [],
2219
- firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt
2890
+ firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt,
2891
+ // Device-local only (the server doesn't hold them).
2892
+ ...this.state.throttle ? { throttle: this.state.throttle } : {},
2893
+ ...this.state.announcementsSeenAt ? { announcementsSeenAt: this.state.announcementsSeenAt } : {}
2220
2894
  };
2221
2895
  if (data.firstSeenAt && Date.now() - Date.parse(data.firstSeenAt) > 6e4) this.firstVisit = false;
2222
2896
  this.persistState();
@@ -2308,6 +2982,8 @@ var GuidelyClient = class {
2308
2982
  ...extra
2309
2983
  };
2310
2984
  if (extra.properties) event.properties = sanitizeProperties(extra.properties);
2985
+ const locale = extra.guideId ? this.guideLocales.get(extra.guideId) : void 0;
2986
+ if (locale) event.properties = { ...event.properties ?? {}, locale };
2311
2987
  this.queue.enqueue(event);
2312
2988
  this.emitter.emit("track", event);
2313
2989
  return event;
@@ -2340,11 +3016,19 @@ var GuidelyClient = class {
2340
3016
  if (!name) return;
2341
3017
  this.whenHydrated(() => {
2342
3018
  const changed = name !== this.screenName;
3019
+ if (changed) {
3020
+ this.leaveScreen();
3021
+ this.flushHeatmap();
3022
+ }
2343
3023
  this.screenName = name;
2344
3024
  this.targets.setScreen(name);
2345
3025
  if (changed) this.syncFlagAttributes();
2346
3026
  const url = this.currentUrl();
2347
- if (changed) this.enqueue("page_view", { properties: { ...properties, path: url, screen: name } });
3027
+ if (changed) {
3028
+ this.screenStartedAt = Date.now();
3029
+ this.screenLeft = this.backgroundedAt !== null;
3030
+ if (this.pageviewsOn()) this.enqueue("page_view", { properties: { ...properties, path: url, screen: name, platform: this.bridge.device.platform } });
3031
+ }
2348
3032
  this.completeChecklistTasksBy("url_visited", url);
2349
3033
  this.active?.controller.resume();
2350
3034
  this.flows.forEach((f) => f.controller.resume());
@@ -2372,6 +3056,149 @@ var GuidelyClient = class {
2372
3056
  if (this.isReady) this.evaluate("click", { targetId });
2373
3057
  }
2374
3058
  // ---------------------------------------------------------------------------
3059
+ // Product analytics (autocapture)
3060
+ // ---------------------------------------------------------------------------
3061
+ captureAllowed() {
3062
+ return !this.destroyed && this.analyticsEnabled();
3063
+ }
3064
+ /** A capability runs when the project enables it (absent on old servers → off), no option turns it off and analytics are allowed. */
3065
+ autocaptureOn(key) {
3066
+ const o = this.options.autocapture;
3067
+ return !!this.config?.settings?.autocapture?.[key] && o !== false && !(typeof o === "object" && o?.[key] === false) && this.captureAllowed();
3068
+ }
3069
+ /** `page_view` from `screen()` predates autocapture: only an explicit opt-out stops it. */
3070
+ pageviewsOn() {
3071
+ const o = this.options.autocapture;
3072
+ return this.config?.settings?.autocapture?.pageviews !== false && o !== false && !(typeof o === "object" && o?.pageviews === false);
3073
+ }
3074
+ /** True when taps are worth observing (clicks, rage taps or heatmaps are on); the provider skips resolving views otherwise. */
3075
+ isTapCaptureOn() {
3076
+ return this.hydrated && (this.autocaptureOn("clicks") || this.autocaptureOn("rageClicks") || this.autocaptureOn("heatmaps"));
3077
+ }
3078
+ /**
3079
+ * Records a tap observed by the provider: `click` for an interactive element, `rage_click` for
3080
+ * bursts and a heatmap point. A registered `<GuidelyTarget>` whose frame contains the point
3081
+ * (smallest wins) takes precedence over the resolved element and is reported as `#<id>`.
3082
+ */
3083
+ recordTap(tap) {
3084
+ try {
3085
+ if (!this.hydrated || this.destroyed || !Number.isFinite(tap.x) || !Number.isFinite(tap.y)) return;
3086
+ const on = { clicks: this.autocaptureOn("clicks"), rage: this.autocaptureOn("rageClicks"), heat: this.autocaptureOn("heatmaps") };
3087
+ if (!on.clicks && !on.rage && !on.heat) return;
3088
+ const t = tap.t ?? Date.now();
3089
+ const url = this.screenName ? this.currentUrl() : "";
3090
+ this.tapChain = this.tapChain.then(() => this.processTap(tap, t, url, on)).catch((err) => this.reportError(err));
3091
+ } catch (err) {
3092
+ this.reportError(err);
3093
+ }
3094
+ }
3095
+ async processTap(tap, t, url, on) {
3096
+ let el = tap.element ?? null;
3097
+ let rect = null;
3098
+ const target = this.targets.ids().length ? await this.targets.hitTest(tap.x, tap.y) : null;
3099
+ if (target) {
3100
+ el = { selector: `#${target.id}`, tag: el?.tag ?? tap.hit?.tag ?? "View", text: el?.text };
3101
+ rect = target.rect;
3102
+ }
3103
+ if (this.destroyed) return;
3104
+ const platform = this.bridge.device.platform;
3105
+ const maskAll = !!this.config?.settings?.replay?.maskAllText;
3106
+ const eventUrl = this.collectUrls() && url ? url : void 0;
3107
+ if (on.clicks && el) this.enqueue("click", { url: eventUrl, properties: { ...tapProperties(el, tap.x, tap.y, platform, maskAll) } });
3108
+ const count = this.rage.add(tap.x, tap.y, t);
3109
+ if (on.rage && count) this.enqueue("rage_click", { url: eventUrl, properties: { ...tapProperties(el ?? tap.hit ?? UNKNOWN_VIEW, tap.x, tap.y, platform, maskAll), count } });
3110
+ if (!on.heat) return;
3111
+ const burst = this.rage.burst;
3112
+ if (el && !rect && el.measure) rect = await el.measure().catch(() => null);
3113
+ if (this.destroyed) return;
3114
+ if (this.heatmap.url !== url) this.flushHeatmap();
3115
+ if (this.heatmap.add(url, heatmapPoint(tap.x, tap.y, el, rect, burst > 0), burst)) this.flushHeatmap();
3116
+ }
3117
+ /** Sends the buffered heatmap taps as one `heatmap` event attributed to the screen they happened on. */
3118
+ flushHeatmap() {
3119
+ const batch = this.heatmap.take();
3120
+ if (!batch || !this.captureAllowed()) return;
3121
+ const collect = this.collectUrls() && !!batch.url;
3122
+ const { width, height } = this.windowSize();
3123
+ this.enqueue("heatmap", {
3124
+ url: collect ? batch.url : void 0,
3125
+ properties: { ...collect ? { path: batch.url } : {}, viewportWidth: width, viewportHeight: height, pageHeight: height, platform: this.bridge.device.platform, points: batch.points }
3126
+ });
3127
+ }
3128
+ windowSize() {
3129
+ try {
3130
+ const s = this.bridge.windowSize?.();
3131
+ if (s && Number.isFinite(s.width) && Number.isFinite(s.height)) return { width: Math.round(s.width), height: Math.round(s.height) };
3132
+ } catch {
3133
+ }
3134
+ return { width: 0, height: 0 };
3135
+ }
3136
+ /** Ends the foreground measurement of the current screen with a `page_leave` (once per measurement). */
3137
+ leaveScreen() {
3138
+ if (!this.screenName || this.screenLeft) return;
3139
+ this.screenLeft = true;
3140
+ if (!this.autocaptureOn("pageLeave")) return;
3141
+ const url = this.currentUrl();
3142
+ const collect = this.collectUrls();
3143
+ this.enqueue("page_leave", {
3144
+ url: collect ? url : void 0,
3145
+ properties: { ...collect ? { path: url } : {}, screen: this.screenName, durationMs: Math.max(0, Date.now() - this.screenStartedAt), scrollDepth: 100, platform: this.bridge.device.platform }
3146
+ });
3147
+ }
3148
+ /** `{ appVersion, build }` of this launch, without unknown keys. */
3149
+ appInfo() {
3150
+ const appVersion = this.options.appVersion ?? this.bridge.device.appVersion;
3151
+ const build = this.options.appBuild;
3152
+ return { ...appVersion ? { appVersion: String(appVersion) } : {}, ...build ? { build: String(build) } : {} };
3153
+ }
3154
+ /**
3155
+ * Once per launch, as soon as a config is known: `app_install` / `app_update` from the stored
3156
+ * version (when the app passes `appVersion` and `storage`), then `app_open` with `coldStart: true`.
3157
+ */
3158
+ checkLaunch() {
3159
+ if (this.launchChecked || !this.config || this.destroyed || !this.hydrated) return;
3160
+ this.launchChecked = true;
3161
+ try {
3162
+ const on = this.autocaptureOn("appLifecycle");
3163
+ const info = this.appInfo();
3164
+ if (info.appVersion && this.options.storage) {
3165
+ const key = appVersionKey(this.options.projectId);
3166
+ const stored = readJson(this.store, key, null);
3167
+ if (!stored?.appVersion) {
3168
+ if (!this.hadIdentity && on) this.enqueue("app_install", { properties: { ...info } });
3169
+ } else if (stored.appVersion !== info.appVersion || !!stored.build && !!info.build && stored.build !== info.build) {
3170
+ if (on) this.enqueue("app_update", { properties: { ...info, previousVersion: stored.appVersion, ...stored.build ? { previousBuild: stored.build } : {} } });
3171
+ }
3172
+ writeJson(this.store, key, info);
3173
+ }
3174
+ if (on) this.enqueue("app_open", { properties: { ...info, coldStart: true } });
3175
+ } catch (err) {
3176
+ this.reportError(err);
3177
+ }
3178
+ }
3179
+ wentToBackground() {
3180
+ try {
3181
+ this.leaveScreen();
3182
+ this.flushHeatmap();
3183
+ if (this.launchChecked && this.autocaptureOn("appLifecycle")) this.enqueue("app_background", { properties: { foregroundMs: Math.max(0, Date.now() - this.openedAt) } });
3184
+ } catch (err) {
3185
+ this.reportError(err);
3186
+ }
3187
+ }
3188
+ cameToForeground() {
3189
+ try {
3190
+ this.openedAt = Date.now();
3191
+ if (this.launchChecked && this.autocaptureOn("appLifecycle")) this.enqueue("app_open", { properties: { ...this.appInfo(), coldStart: false } });
3192
+ if (this.screenName && this.screenLeft) {
3193
+ this.screenStartedAt = Date.now();
3194
+ this.screenLeft = false;
3195
+ if (this.pageviewsOn() && this.autocaptureOn("pageLeave")) this.enqueue("page_view", { properties: { path: this.currentUrl(), screen: this.screenName, platform: this.bridge.device.platform, resumed: true } });
3196
+ }
3197
+ } catch (err) {
3198
+ this.reportError(err);
3199
+ }
3200
+ }
3201
+ // ---------------------------------------------------------------------------
2375
3202
  // Targeting + triggers
2376
3203
  // ---------------------------------------------------------------------------
2377
3204
  /** Builds the targeting context for the current moment. */
@@ -2418,6 +3245,50 @@ var GuidelyClient = class {
2418
3245
  if (guide.kind === "checklist") return !this.state.checklists[guide.id]?.dismissedAt;
2419
3246
  return isFrequencyEligible(guide.config.frequency, this.state.guides[guide.id], this.session.shownGuides.includes(guide.id));
2420
3247
  }
3248
+ /** Guide throttling: may this guide launch automatically now? */
3249
+ throttleAllows(guide) {
3250
+ if (!countsTowardThrottle(guide)) return true;
3251
+ return isThrottleEligible(this.config?.settings.throttling, this.state.throttle ?? []);
3252
+ }
3253
+ recordLaunch(guide, manual) {
3254
+ if (manual || !countsTowardThrottle(guide) || !this.config?.settings.throttling?.enabled) return;
3255
+ this.state.throttle = pruneLaunches([...this.state.throttle ?? [], nowIso()], this.config.settings.throttling);
3256
+ }
3257
+ /** Guides and A/B test control groups, which are evaluated the same way but never render. */
3258
+ candidates() {
3259
+ const guides = this.config?.guides ?? [];
3260
+ const holdouts = this.config?.guideHoldouts ?? [];
3261
+ return holdouts.length ? [...guides, ...holdouts.map((h) => h.guide)] : guides;
3262
+ }
3263
+ holdoutFor(guide) {
3264
+ return this.config?.guideHoldouts?.find((h) => h.guide.id === guide.id) ?? null;
3265
+ }
3266
+ // ---------------------------------------------------------------------------
3267
+ // Localization
3268
+ // ---------------------------------------------------------------------------
3269
+ /** The visitor's language candidates, most preferred first: `setLocale()` / `locale` option, traits, device. */
3270
+ locales() {
3271
+ return visitorLocales(this.locale, this.user?.traits, [this.bridge.device.locale]);
3272
+ }
3273
+ defaultLocale() {
3274
+ return this.config?.settings?.localization?.defaultLocale || "en";
3275
+ }
3276
+ /** The guide in the visitor's language (remembered so its events carry `locale`). */
3277
+ localize(guide) {
3278
+ const localized = localizeGuide(guide, this.locales(), this.defaultLocale());
3279
+ this.guideLocales.set(guide.id, localized.locale);
3280
+ return localized;
3281
+ }
3282
+ /**
3283
+ * Sets the language for guides and the resource center (`null` = back to the user's traits and
3284
+ * the device language). Guides already on screen keep their language; the resource center re-renders.
3285
+ */
3286
+ setLocale(locale) {
3287
+ const next = locale ? normalizeLocale(locale) || null : null;
3288
+ if (next === this.locale) return;
3289
+ this.locale = next;
3290
+ this.notify();
3291
+ }
2421
3292
  triggerFires(guide, reason, detail) {
2422
3293
  const t = guide.config.trigger;
2423
3294
  switch (t.type) {
@@ -2453,7 +3324,7 @@ var GuidelyClient = class {
2453
3324
  this.lastAudiences = current;
2454
3325
  }
2455
3326
  const candidates = [];
2456
- for (const guide of this.config.guides) {
3327
+ for (const guide of this.candidates()) {
2457
3328
  const t = guide.config.trigger;
2458
3329
  if (detail.endedGuide && detail.endedGuide.id === guide.id) continue;
2459
3330
  if (reason === "ready" || reason === "navigation") {
@@ -2469,6 +3340,10 @@ var GuidelyClient = class {
2469
3340
  if (t.type === "page_load" && t.delayMs > 0 && reason === "timer" && detail.guideId !== guide.id) continue;
2470
3341
  if (!this.triggerFires(guide, reason, detail)) continue;
2471
3342
  if (!this.isEligible(guide, ctx, url)) continue;
3343
+ if (!this.throttleAllows(guide)) {
3344
+ this.log("throttled", guide.key);
3345
+ continue;
3346
+ }
2472
3347
  candidates.push(guide);
2473
3348
  }
2474
3349
  if (reason === "after_guide" || reason === "navigation") this.drainQueue(url, ctx);
@@ -2514,6 +3389,12 @@ var GuidelyClient = class {
2514
3389
  }
2515
3390
  tryShow(guide, url, manual) {
2516
3391
  if (this.destroyed) return false;
3392
+ if (!manual && !this.throttleAllows(guide)) return false;
3393
+ if (this.holdoutFor(guide)) {
3394
+ if (this.hasExclusive() && !manual) return false;
3395
+ this.recordImpression(guide, manual);
3396
+ return true;
3397
+ }
2517
3398
  if (!isExclusiveGuide(guide)) {
2518
3399
  if (this.flows.has(guide.id) || this.checklists.has(guide.id)) return false;
2519
3400
  this.show(guide, manual);
@@ -2555,14 +3436,18 @@ var GuidelyClient = class {
2555
3436
  guideState(id) {
2556
3437
  return this.state.guides[id] ?? (this.state.guides[id] = { impressions: 0 });
2557
3438
  }
2558
- recordImpression(guide) {
3439
+ recordImpression(guide, manual = false) {
2559
3440
  const s = this.guideState(guide.id);
2560
3441
  s.impressions += 1;
2561
3442
  s.lastShownAt = nowIso();
2562
3443
  if (!this.session.shownGuides.includes(guide.id)) this.session.shownGuides.push(guide.id);
3444
+ this.recordLaunch(guide, manual);
2563
3445
  this.persistState();
2564
3446
  this.persistSession();
2565
- this.syncGuideState(guide.id, { impression: true });
3447
+ const holdout = this.holdoutFor(guide);
3448
+ const experiment = holdout ? { id: holdout.experimentId, variantId: holdout.variantId } : guide.experiment;
3449
+ if (experiment) this.enqueue("experiment_viewed", { properties: { guideExperimentId: experiment.id, variantId: experiment.variantId, ...holdout ? {} : { guideId: guide.id } } });
3450
+ if (!holdout) this.syncGuideState(guide.id, { impression: true });
2566
3451
  }
2567
3452
  recordEnd(guide, reason) {
2568
3453
  const s = this.guideState(guide.id);
@@ -2573,7 +3458,13 @@ var GuidelyClient = class {
2573
3458
  if (reason === "completed") this.completeChecklistTasksBy("guide_completed", guide);
2574
3459
  }
2575
3460
  syncGuideState(guideId, update) {
2576
- void postWithRetry(this.api, "/guide-state", { anonymousId: this.anonymousId, userId: this.user?.id || null, updates: [{ guideId, ...update }] });
3461
+ const body = { anonymousId: this.anonymousId, userId: this.user?.id || null, updates: [{ guideId, ...update }] };
3462
+ if (update.impression) void postWithRetry(this.api, "/guide-state", body);
3463
+ else this.sendDurably(`guide:${guideId}:${update.completed ? "completed" : "dismissed"}:${this.anonymousId}`, "/guide-state", body);
3464
+ }
3465
+ /** Queues a write in the persisted outbox (sent now, or once the API is reachable again). */
3466
+ sendDurably(key, path, body) {
3467
+ this.whenHydrated(() => this.outbox?.add(key, path, body));
2577
3468
  }
2578
3469
  show(guide, manual, stepId) {
2579
3470
  try {
@@ -2584,7 +3475,8 @@ var GuidelyClient = class {
2584
3475
  this.reportError(err);
2585
3476
  }
2586
3477
  }
2587
- showFlow(guide, manual, stepId) {
3478
+ showFlow(raw, manual, stepId) {
3479
+ const guide = this.localize(raw);
2588
3480
  const exclusive = isExclusiveGuide(guide);
2589
3481
  const flow = { guide, manual, view: null, controller: null };
2590
3482
  let renders = 0;
@@ -2599,7 +3491,7 @@ var GuidelyClient = class {
2599
3491
  const step = e.stepId ? guide.config.steps.find((s) => s.id === e.stepId) : void 0;
2600
3492
  this.enqueue(e.name, { guideId: guide.id, guideVersion: guide.version, stepId: e.stepId, stepIndex: e.stepIndex, properties: e.properties });
2601
3493
  if (e.name === "guide_started") {
2602
- this.recordImpression(guide);
3494
+ this.recordImpression(guide, manual);
2603
3495
  this.emitter.emit("guide:start", guide);
2604
3496
  } else if (e.name === "step_viewed" && step) {
2605
3497
  this.emitter.emit("guide:step", { guide, stepId: step.id, stepIndex: e.stepIndex ?? 0 });
@@ -2646,21 +3538,24 @@ var GuidelyClient = class {
2646
3538
  if (this.flows.get(guide.id) === flow) this.flows.delete(guide.id);
2647
3539
  }
2648
3540
  }
2649
- showChecklist(guide, open) {
3541
+ showChecklist(raw, open) {
3542
+ if (!raw.config.checklist || this.checklists.has(raw.id)) return;
3543
+ const guide = this.localize(raw);
2650
3544
  const config = guide.config.checklist;
2651
- if (!config || this.checklists.has(guide.id)) return;
3545
+ if (!config) return;
2652
3546
  const progress = this.getChecklistProgress(guide.id);
2653
3547
  const first = !this.state.guides[guide.id]?.impressions;
2654
- this.checklists.set(guide.id, { open: open ?? (config.autoOpen && first && !progress.completedAt) });
3548
+ this.checklists.set(guide.id, { guide, open: open ?? (config.autoOpen && first && !progress.completedAt) });
2655
3549
  if (first) this.recordImpression(guide);
2656
3550
  if (this.checklists.get(guide.id)?.open) this.enqueue("guide_viewed", { guideId: guide.id, guideVersion: guide.version });
2657
3551
  this.notify();
2658
3552
  }
2659
- showSurvey(guide, manual) {
2660
- if (!guide.config.survey) return;
3553
+ showSurvey(raw, manual) {
3554
+ if (!raw.config.survey) return;
3555
+ const guide = this.localize(raw);
2661
3556
  this.activeSurvey = { guide, manual };
2662
3557
  this.surveyView = { key: `${guide.id}:${randomId(6)}`, guide, submitted: false };
2663
- this.recordImpression(guide);
3558
+ this.recordImpression(guide, manual);
2664
3559
  this.enqueue("survey_displayed", { guideId: guide.id, guideVersion: guide.version });
2665
3560
  this.emitter.emit("guide:start", guide);
2666
3561
  this.notify();
@@ -2720,10 +3615,7 @@ var GuidelyClient = class {
2720
3615
  const steps = [];
2721
3616
  for (const f of [...this.flows.values(), ...this.active ? [this.active] : []]) if (f.view) steps.push({ ...f.view, expanded: this.expanded.has(f.guide.id) });
2722
3617
  const checklists = [];
2723
- this.checklists.forEach((w, id) => {
2724
- const guide = this.findGuide(id);
2725
- if (guide) checklists.push({ guide, progress: this.getChecklistProgress(id), open: w.open });
2726
- });
3618
+ this.checklists.forEach((w, id) => checklists.push({ guide: w.guide, progress: this.getChecklistProgress(id), open: w.open }));
2727
3619
  this.ui = { steps, checklists, survey: this.surveyView, resourceCenterOpen: this.resourceCenterOpen, theme: this.themeFor(), messenger: !!this.messenger };
2728
3620
  for (const cb of Array.from(this.uiListeners)) {
2729
3621
  try {
@@ -2781,7 +3673,7 @@ var GuidelyClient = class {
2781
3673
  progress.dismissedAt = nowIso();
2782
3674
  this.persistState();
2783
3675
  this.enqueue("checklist_dismissed", { guideId: guide.id, guideVersion: guide.version });
2784
- void postWithRetry(this.api, `/checklists/${encodeURIComponent(guide.id)}/progress`, { anonymousId: this.anonymousId, userId: this.user?.id || null, guideVersion: guide.version, completedTaskIds: progress.completedTaskIds, dismissed: true });
3676
+ this.sendDurably(`checklist:${guide.id}:dismissed:${this.anonymousId}`, `/checklists/${encodeURIComponent(guide.id)}/progress`, { anonymousId: this.anonymousId, userId: this.user?.id || null, guideVersion: guide.version, completedTaskIds: progress.completedTaskIds, dismissed: true });
2785
3677
  this.checklists.delete(guide.id);
2786
3678
  this.notify();
2787
3679
  }
@@ -2794,7 +3686,7 @@ var GuidelyClient = class {
2794
3686
  this.persistState();
2795
3687
  this.recordEnd(guide, "completed");
2796
3688
  this.enqueue("survey_submitted", { guideId: guide.id, guideVersion: guide.version, properties: { responseId } });
2797
- void postWithRetry(this.api, `/surveys/${encodeURIComponent(guide.id)}/responses`, { anonymousId: this.anonymousId, userId: this.user?.id || null, accountId: this.user?.account?.id || null, guideVersion: guide.version, answers, responseId });
3689
+ this.sendDurably(`survey:${responseId}`, `/surveys/${encodeURIComponent(guide.id)}/responses`, { anonymousId: this.anonymousId, userId: this.user?.id || null, accountId: this.user?.account?.id || null, guideVersion: guide.version, answers, responseId });
2798
3690
  this.emitter.emit("survey:submit", { guide, answers, responseId });
2799
3691
  this.emitter.emit("guide:complete", guide);
2800
3692
  this.surveyView = { ...this.surveyView, submitted: true };
@@ -2808,10 +3700,58 @@ var GuidelyClient = class {
2808
3700
  getAudienceIds() {
2809
3701
  return this.state.audienceIds;
2810
3702
  }
3703
+ /** The published resource center in the visitor's language, or null when it is off. */
2811
3704
  getResourceCenter() {
2812
- const rc = this.config?.resourceCenter;
3705
+ const rc = this.localizedResourceCenter();
2813
3706
  return rc && rc.enabled ? rc : null;
2814
3707
  }
3708
+ /** The resource center in the visitor's language (cached until the config or language changes). */
3709
+ localizedResourceCenter() {
3710
+ const rc = this.config?.resourceCenter;
3711
+ if (!rc) return null;
3712
+ const locales = this.locales();
3713
+ const key = `${locales.join(",")}|${this.defaultLocale()}`;
3714
+ if (this.localizedRc?.source !== rc || this.localizedRc.locales !== key) this.localizedRc = { source: rc, locales: key, value: localizeResourceCenter(rc, locales, this.defaultLocale()) };
3715
+ return this.localizedRc.value;
3716
+ }
3717
+ /** Evaluates targeting rules (e.g. a resource-center section's) for the current visitor and screen. */
3718
+ matchesTargeting(rules) {
3719
+ if (!rules) return true;
3720
+ try {
3721
+ return evaluateRules(rules, this.getContext());
3722
+ } catch (err) {
3723
+ this.reportError(err);
3724
+ return false;
3725
+ }
3726
+ }
3727
+ /** Resource-center sections this visitor sees, in order (disabled and non-matching ones left out). */
3728
+ getResourceCenterModules() {
3729
+ const rc = this.getResourceCenter();
3730
+ if (!rc) return [];
3731
+ return orderedModules(rc.modules).filter((m) => m.enabled && (!("targeting" in m) || this.matchesTargeting(m.targeting))).map((m) => m.id);
3732
+ }
3733
+ /**
3734
+ * Sections of the messenger's Help tab, in order: the resource center's modules without "What's
3735
+ * new" (the messenger has its own News tab); every section when no resource center is published.
3736
+ */
3737
+ getHelpModules() {
3738
+ const modules = this.getResourceCenter() ? this.getResourceCenterModules() : [...RC_MODULE_IDS];
3739
+ return modules.filter((id) => id !== "announcements");
3740
+ }
3741
+ /** "What's new" entries this visitor sees (newest first); empty when the section is off. */
3742
+ getAnnouncements() {
3743
+ const rc = this.getResourceCenter();
3744
+ if (!rc || !this.getResourceCenterModules().includes("announcements")) return [];
3745
+ return visibleAnnouncements(rc.announcements, this.state.audienceIds);
3746
+ }
3747
+ /** "What's new" entries published since the visitor last opened the resource center. */
3748
+ getUnreadAnnouncements() {
3749
+ return unreadAnnouncements(this.getAnnouncements(), this.state.announcementsSeenAt);
3750
+ }
3751
+ /** Whether an entry is new to the visitor in the resource center that is open now. */
3752
+ isAnnouncementNew(a) {
3753
+ return unreadAnnouncements([a], this.announcementsSeenAtOpen) > 0;
3754
+ }
2815
3755
  async searchResourceCenter(q) {
2816
3756
  try {
2817
3757
  const res = await this.api.get("/resource-center/search", { params: { q } });
@@ -2820,6 +3760,11 @@ var GuidelyClient = class {
2820
3760
  return [];
2821
3761
  }
2822
3762
  }
3763
+ /** The language of the resource center as shown, when it isn't the default (sent with article fetches). */
3764
+ articleLocale() {
3765
+ const locale = this.localizedResourceCenter()?.locale;
3766
+ return locale && locale.toLowerCase() !== this.defaultLocale().toLowerCase() ? locale : void 0;
3767
+ }
2823
3768
  articleViewed(article) {
2824
3769
  this.enqueue("article_viewed", { properties: { articleId: article.id, slug: article.slug } });
2825
3770
  }
@@ -2851,7 +3796,7 @@ var GuidelyClient = class {
2851
3796
  this.enqueue("checklist_completed", { guideId: guide.id, guideVersion: guide.version });
2852
3797
  }
2853
3798
  this.persistState();
2854
- void postWithRetry(this.api, `/checklists/${encodeURIComponent(guide.id)}/progress`, { anonymousId: this.anonymousId, userId: this.user?.id || null, guideVersion: guide.version, completedTaskIds: progress.completedTaskIds, completed: !!progress.completedAt });
3799
+ this.sendDurably(`checklist:${guide.id}:progress:${this.anonymousId}`, `/checklists/${encodeURIComponent(guide.id)}/progress`, { anonymousId: this.anonymousId, userId: this.user?.id || null, guideVersion: guide.version, completedTaskIds: progress.completedTaskIds, completed: !!progress.completedAt });
2855
3800
  this.emitter.emit("checklist:task", { guide, task, progress: this.getChecklistProgress(guide.id) });
2856
3801
  if (complete) {
2857
3802
  this.emitter.emit("checklist:complete", guide);
@@ -2954,6 +3899,11 @@ var GuidelyClient = class {
2954
3899
  }
2955
3900
  if (this.resourceCenterOpen) return;
2956
3901
  this.resourceCenterOpen = true;
3902
+ this.announcementsSeenAtOpen = this.state.announcementsSeenAt ?? null;
3903
+ if (this.getAnnouncements().length) {
3904
+ this.state.announcementsSeenAt = nowIso();
3905
+ this.persistState();
3906
+ }
2957
3907
  this.enqueue("resource_center_opened");
2958
3908
  this.emitter.emit("resource-center:open");
2959
3909
  this.notify();
@@ -2988,7 +3938,9 @@ var GuidelyClient = class {
2988
3938
  },
2989
3939
  onMessage: (message, conversation) => this.emitter.emit("conversation:message", { message, conversation }),
2990
3940
  onStartGuide: (key) => this.setTimer(() => this.startGuide(key), 0),
2991
- onError: (err) => this.log("messenger", err instanceof Error ? err.message : err)
3941
+ onError: (err) => this.log("messenger", err instanceof Error ? err.message : err),
3942
+ health: this.api.health,
3943
+ locale: () => this.articleLocale()
2992
3944
  });
2993
3945
  this.resourceCenterOpen = false;
2994
3946
  }
@@ -3093,6 +4045,7 @@ var GuidelyClient = class {
3093
4045
  audienceIds: this.state.audienceIds,
3094
4046
  session: this.session,
3095
4047
  pendingEvents: this.queue?.size ?? 0,
4048
+ pendingWrites: this.outbox?.size ?? 0,
3096
4049
  targets: this.targets.ids(),
3097
4050
  flagsReady: this.flags.isReady()
3098
4051
  };
@@ -3121,7 +4074,191 @@ var GuidelyClient = class {
3121
4074
  }
3122
4075
  };
3123
4076
  function forPlatform(config, platform) {
3124
- return config.guides.every((g) => supportsPlatform(g.config, platform)) ? config : { ...config, guides: config.guides.filter((g) => supportsPlatform(g.config, platform)) };
4077
+ const keep = (g) => supportsPlatform(g.config, platform);
4078
+ const holdouts = config.guideHoldouts?.filter((h) => keep(h.guide)).map((h) => ({ ...h, guide: withoutCode(h.guide) }));
4079
+ return { ...config, guides: config.guides.filter(keep).map(withoutCode), ...holdouts ? { guideHoldouts: holdouts } : {} };
4080
+ }
4081
+ function withoutCode(guide) {
4082
+ if (!guide.config.steps.some((s) => s.code)) return guide;
4083
+ return { ...guide, config: { ...guide.config, steps: guide.config.steps.map(({ code: _code, ...step }) => step) } };
4084
+ }
4085
+
4086
+ // src/ui/Autocapture.tsx
4087
+ var import_react8 = require("react");
4088
+ var import_react_native11 = require("react-native");
4089
+
4090
+ // src/core/viewTree.ts
4091
+ var HOST_TEXT = 6;
4092
+ var MAX_ANCESTORS = 500;
4093
+ var MAX_TEXT_NODES = 300;
4094
+ var INTERACTIVE_ROLES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem", "checkbox", "switch", "radio", "togglebutton", "imagebutton", "option", "combobox", "adjustable"]);
4095
+ var TEXT_INPUT_HOST = /TextInput/i;
4096
+ var NOISE = /^(Anonymous|Unknown|ForwardRef|Memo|Context|Provider|Consumer|Fragment|StrictMode|Suspense|Profiler)$|Context$|Provider$|Consumer$/;
4097
+ var isFiber = (v) => !!v && typeof v === "object" && "return" in v && "memoizedProps" in v;
4098
+ function fiberFromEvent(event) {
4099
+ try {
4100
+ const e = event;
4101
+ if (isFiber(e?._targetInst)) return e._targetInst;
4102
+ const target = e?.target;
4103
+ if (!target || typeof target !== "object") return null;
4104
+ for (const key of ["__internalInstanceHandle", "_internalInstanceHandle", "_internalFiberInstanceHandleDEV"]) if (isFiber(target[key])) return target[key];
4105
+ for (const sym of Object.getOwnPropertySymbols(target)) if (sym.description === "internalInstanceHandle" && isFiber(target[sym])) return target[sym];
4106
+ } catch {
4107
+ }
4108
+ return null;
4109
+ }
4110
+ function componentName(type, depth = 0) {
4111
+ if (!type || depth > 3) return null;
4112
+ if (typeof type === "function") {
4113
+ const f = type;
4114
+ const n = typeof f.displayName === "string" && f.displayName ? f.displayName : typeof f.name === "string" ? f.name : "";
4115
+ return n || null;
4116
+ }
4117
+ if (typeof type === "object") {
4118
+ const o = type;
4119
+ if (typeof o.displayName === "string" && o.displayName) return o.displayName;
4120
+ return componentName(o.render, depth + 1) ?? componentName(o.type, depth + 1);
4121
+ }
4122
+ return null;
4123
+ }
4124
+ var isHost = (f) => typeof f.type === "string";
4125
+ var isTextInput = (f) => typeof f.type === "string" ? TEXT_INPUT_HOST.test(f.type) : componentName(f.type) === "TextInput";
4126
+ function isInteractive(props) {
4127
+ if (!props || typeof props !== "object") return null;
4128
+ if (typeof props.onPress === "function" || typeof props.onLongPress === "function") return "press";
4129
+ const role = props.accessibilityRole ?? props.role;
4130
+ return typeof role === "string" && INTERACTIVE_ROLES.has(role) ? "role" : null;
4131
+ }
4132
+ function firstHost(f) {
4133
+ let cur = f;
4134
+ for (let i = 0; cur && i < 30; i++, cur = cur.child) if (isHost(cur)) return cur;
4135
+ return null;
4136
+ }
4137
+ function idSelector(f) {
4138
+ const host = firstHost(f);
4139
+ const sources = [f.memoizedProps, host?.memoizedProps];
4140
+ for (const key of ["nativeID", "testID"]) {
4141
+ for (const p of sources) {
4142
+ const v = p && typeof p === "object" ? p[key] : void 0;
4143
+ if (typeof v === "string" && v) return attrSelector(key, v);
4144
+ }
4145
+ }
4146
+ return null;
4147
+ }
4148
+ function pathSelector(f) {
4149
+ const parts = [];
4150
+ let cur = f;
4151
+ for (let i = 0; cur && parts.length < 4 && i < MAX_ANCESTORS; i++, cur = cur.return) {
4152
+ if (isHost(cur)) continue;
4153
+ const name = componentName(cur.type);
4154
+ if (!name || NOISE.test(name)) continue;
4155
+ let same = 0;
4156
+ let index = 0;
4157
+ for (let s = cur.return?.child, n = 0; s && n < 500; s = s.sibling, n++) {
4158
+ if (s.type !== cur.type) continue;
4159
+ same += 1;
4160
+ if (s === cur) index = same;
4161
+ }
4162
+ parts.unshift(same > 1 && index ? `${name}:nth-of-type(${index})` : name);
4163
+ }
4164
+ return parts.join(" > ").slice(0, 300) || "View";
4165
+ }
4166
+ function subtreeText(f, opts) {
4167
+ const out = [];
4168
+ let visited = 0;
4169
+ let masked = false;
4170
+ const walk = (node) => {
4171
+ for (let n = node; n && visited < MAX_TEXT_NODES && !masked; n = n.sibling) {
4172
+ visited += 1;
4173
+ if (opts.isMask?.(n.type)) {
4174
+ masked = true;
4175
+ return;
4176
+ }
4177
+ if (isTextInput(n)) continue;
4178
+ if (n.tag === HOST_TEXT && (typeof n.memoizedProps === "string" || typeof n.memoizedProps === "number")) out.push(String(n.memoizedProps));
4179
+ else walk(n.child);
4180
+ }
4181
+ };
4182
+ if (opts.isMask?.(f.type)) return { text: "", masked: true };
4183
+ walk(f.child);
4184
+ return { text: out.join(" "), masked };
4185
+ }
4186
+ function measureHost(host) {
4187
+ return new Promise((resolve) => {
4188
+ let settled = false;
4189
+ const done = (r) => {
4190
+ if (settled) return;
4191
+ settled = true;
4192
+ clearTimeout(timer);
4193
+ resolve(r);
4194
+ };
4195
+ const timer = setTimeout(() => done(null), 500);
4196
+ const cb = (x, y, width, height) => done(Number.isFinite(x) && Number.isFinite(y) && width > 0 && height > 0 ? { x, y, width, height } : null);
4197
+ try {
4198
+ const sn = host.stateNode;
4199
+ const pub = sn?.canonical?.publicInstance ?? sn;
4200
+ if (pub && typeof pub.measureInWindow === "function") return pub.measureInWindow(cb);
4201
+ const ui = globalThis.nativeFabricUIManager;
4202
+ if (sn?.node && typeof ui?.measureInWindow === "function") return ui.measureInWindow(sn.node, cb);
4203
+ } catch {
4204
+ }
4205
+ done(null);
4206
+ });
4207
+ }
4208
+ function describe(f, masked, opts, withText) {
4209
+ const props = f.memoizedProps && typeof f.memoizedProps === "object" ? f.memoizedProps : {};
4210
+ const host = firstHost(f);
4211
+ const el = { selector: idSelector(f) ?? pathSelector(f), tag: componentName(f.type) ?? "View" };
4212
+ if (withText && !masked) {
4213
+ const sub = subtreeText(f, opts);
4214
+ if (!sub.masked) {
4215
+ const hostProps = host?.memoizedProps && typeof host.memoizedProps === "object" ? host.memoizedProps : {};
4216
+ const label = props.accessibilityLabel ?? props["aria-label"] ?? hostProps.accessibilityLabel ?? hostProps["aria-label"];
4217
+ const text = cleanText(sub.text) ?? (typeof label === "string" ? cleanText(label) : void 0);
4218
+ if (text) el.text = text;
4219
+ }
4220
+ }
4221
+ if (host) el.measure = () => measureHost(host);
4222
+ return el;
4223
+ }
4224
+ function resolveFiber(target, opts = {}) {
4225
+ const none = { ignored: false, element: null, hit: null };
4226
+ if (!target) return none;
4227
+ try {
4228
+ let press = null;
4229
+ let role = null;
4230
+ let hitFiber = null;
4231
+ let stopped = false;
4232
+ let masked = false;
4233
+ for (let f = target, i = 0; f && i < MAX_ANCESTORS; f = f.return, i++) {
4234
+ if (opts.isIgnored?.(f.type)) return { ignored: true, element: null, hit: null };
4235
+ if (opts.isMask?.(f.type)) masked = true;
4236
+ if (stopped) continue;
4237
+ if (isTextInput(f)) {
4238
+ stopped = true;
4239
+ continue;
4240
+ }
4241
+ if (isHost(f)) continue;
4242
+ const name = componentName(f.type);
4243
+ if (!hitFiber && name && !NOISE.test(name)) hitFiber = f;
4244
+ const kind = isInteractive(f.memoizedProps);
4245
+ if (kind === "press") {
4246
+ press = f;
4247
+ stopped = true;
4248
+ } else if (kind === "role" && !role) role = f;
4249
+ }
4250
+ const element = press ?? role;
4251
+ return {
4252
+ ignored: false,
4253
+ element: element ? describe(element, masked, opts, true) : null,
4254
+ hit: hitFiber ? describe(hitFiber, masked, opts, componentName(hitFiber.type) === "Text") : null
4255
+ };
4256
+ } catch {
4257
+ return none;
4258
+ }
4259
+ }
4260
+ function resolveTapTarget(event, opts = {}) {
4261
+ return resolveFiber(fiberFromEvent(event), opts);
3125
4262
  }
3126
4263
 
3127
4264
  // src/ui/Overlay.tsx
@@ -3881,25 +5018,41 @@ function Help({ store, client, palette: p }) {
3881
5018
  ] }, title);
3882
5019
  const audiences = client.getAudienceIds();
3883
5020
  const sections = resourceCenterSections(articles?.collections ?? [], articles?.articles ?? [], audiences);
3884
- const listed = results?.filter((a) => isArticleListed(articles?.collections ?? [], a, audiences));
5021
+ const modules = client.getHelpModules();
5022
+ const searchOn = modules.includes("search");
5023
+ const listed = searchOn ? results?.filter((a) => isArticleListed(articles?.collections ?? [], a, audiences)) : void 0;
5024
+ const checklists = (rc?.checklistGuideIds ?? []).map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g && g.kind === "checklist");
5025
+ const renderModule = (id) => {
5026
+ switch (id) {
5027
+ case "guides":
5028
+ return recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u25B6"))) : null;
5029
+ case "checklists":
5030
+ return checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u2611"))) : null;
5031
+ case "articles":
5032
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
5033
+ !articles ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.ActivityIndicator, { color: p.muted }) : null,
5034
+ sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => open(a)))))
5035
+ ] });
5036
+ case "faqs":
5037
+ return rc?.faqs.length ? section(
5038
+ "FAQ",
5039
+ rc.faqs.map((f) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.View, { style: [styles4.row, styles4.faq, { borderColor: p.border }], children: [
5040
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.Pressable, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles4.faqHead, children: [
5041
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.rowTitle, styles4.flex, { color: p.text }], children: f.question }),
5042
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
5043
+ ] }),
5044
+ faq === f.id ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
5045
+ ] }, f.id))
5046
+ ) : null;
5047
+ case "links":
5048
+ return rc?.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null;
5049
+ default:
5050
+ return null;
5051
+ }
5052
+ };
3885
5053
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.ScrollView, { contentContainerStyle: styles4.content, keyboardShouldPersistTaps: "handled", children: [
3886
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.TextInput, { value: q, onChangeText: setQ, placeholder: "Search articles", placeholderTextColor: p.muted, accessibilityLabel: "Search help articles", returnKeyType: "search", style: [styles4.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }] }),
3887
- listed ? section(`Results for \u201C${q.trim()}\u201D`, listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => open(a))) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.rowSub, { color: p.muted }], children: "No articles found." })) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
3888
- recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u25B6"))) : null,
3889
- !articles ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.ActivityIndicator, { color: p.muted }) : null,
3890
- sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => open(a))))),
3891
- rc?.faqs.length ? section(
3892
- "FAQ",
3893
- rc.faqs.map((f) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.View, { style: [styles4.row, styles4.faq, { borderColor: p.border }], children: [
3894
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.Pressable, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles4.faqHead, children: [
3895
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.rowTitle, styles4.flex, { color: p.text }], children: f.question }),
3896
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
3897
- ] }),
3898
- faq === f.id ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
3899
- ] }, f.id))
3900
- ) : null,
3901
- rc?.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null
3902
- ] })
5054
+ searchOn ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.TextInput, { value: q, onChangeText: setQ, placeholder: "Search articles", placeholderTextColor: p.muted, accessibilityLabel: "Search help articles", returnKeyType: "search", style: [styles4.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }] }) : null,
5055
+ listed ? section(`Results for \u201C${q.trim()}\u201D`, listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => open(a))) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.rowSub, { color: p.muted }], children: "No articles found." })) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: modules.filter((id) => id !== "search").map((id) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react3.Fragment, { children: renderModule(id) }, id)) })
3903
5056
  ] });
3904
5057
  }
3905
5058
  function Article({ article, palette: p, client }) {
@@ -4002,6 +5155,8 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4002
5155
  const checklists = (0, import_react4.useMemo)(() => (rc?.checklistGuideIds ?? []).map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g && g.kind === "checklist"), [rc, guides]);
4003
5156
  if (!rc) return null;
4004
5157
  const close = () => client.closeResourceCenter();
5158
+ const modules = client.getResourceCenterModules();
5159
+ const announcements = client.getAnnouncements();
4005
5160
  const openArticle = (a) => {
4006
5161
  if (a.externalUrl) return client.navigate(a.externalUrl);
4007
5162
  client.articleViewed(a);
@@ -4020,7 +5175,51 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4020
5175
  ] }, title);
4021
5176
  const audiences = client.getAudienceIds();
4022
5177
  const sections = resourceCenterSections(rc.collections, rc.articles, audiences);
4023
- const listed = results?.filter((a) => isArticleListed(rc.collections, a, audiences));
5178
+ const listed = modules.includes("search") ? results?.filter((a) => isArticleListed(rc.collections, a, audiences)) : void 0;
5179
+ const link = (label, onPress) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress, hitSlop: 8, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.link, { color: p.primary }], children: label }) }, label);
5180
+ const announcement2 = (a) => {
5181
+ const linked = a.articleId ? rc.articles.find((x) => x.id === a.articleId) : void 0;
5182
+ const isNew = client.isAnnouncementNew(a);
5183
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
5184
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: styles5.faqHead, children: [
5185
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { accessibilityRole: "header", style: [styles5.rowTitle, styles5.flex, { color: p.text, fontFamily: p.fontFamily }], children: a.title }),
5186
+ isNew ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.badge, { color: p.onPrimary, backgroundColor: p.primary }], children: "NEW" }) : null
5187
+ ] }),
5188
+ a.body ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RichText, { html: a.body, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null,
5189
+ a.guideId || linked || a.url ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: styles5.links, children: [
5190
+ a.guideId ? link("Show me", () => (close(), client.startGuide(a.guideId))) : null,
5191
+ linked ? link("Read more", () => openArticle(linked)) : null,
5192
+ a.url ? link(a.linkLabel || "Learn more", () => client.navigate(a.url)) : null
5193
+ ] }) : null
5194
+ ] }, a.id);
5195
+ };
5196
+ const renderModule = (id) => {
5197
+ switch (id) {
5198
+ case "announcements":
5199
+ return announcements.length ? section("What's new", announcements.map(announcement2)) : null;
5200
+ case "guides":
5201
+ return recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => (close(), client.startGuide(g.id)), "\u25B6"))) : null;
5202
+ case "checklists":
5203
+ return checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => (close(), client.openChecklist(g.id)), "\u2611"))) : null;
5204
+ case "articles":
5205
+ return sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => openArticle(a)))));
5206
+ case "faqs":
5207
+ return rc.faqs.length ? section(
5208
+ "FAQ",
5209
+ rc.faqs.map((f) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
5210
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.Pressable, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles5.faqHead, children: [
5211
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.rowTitle, styles5.flex, { color: p.text }], children: f.question }),
5212
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
5213
+ ] }),
5214
+ faq === f.id ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
5215
+ ] }, f.id))
5216
+ ) : null;
5217
+ case "links":
5218
+ return rc.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null;
5219
+ default:
5220
+ return null;
5221
+ }
5222
+ };
4024
5223
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Modal, { visible: open, animationType: "slide", presentationStyle: import_react_native6.Platform.OS === "ios" ? "pageSheet" : "fullScreen", onRequestClose: () => article ? setArticle(null) : close(), children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.sheet, { backgroundColor: p.background, paddingTop: import_react_native6.Platform.OS === "ios" ? 8 : topInset }], children: [
4025
5224
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.header, { borderColor: p.border }], children: [
4026
5225
  article ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Pressable, { accessibilityRole: "button", accessibilityLabel: "Back", onPress: () => setArticle(null), hitSlop: 10, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.back, { color: p.primary }], children: "\u2039 Back" }) }) : null,
@@ -4031,7 +5230,7 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4031
5230
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { accessibilityRole: "header", style: [styles5.articleTitle, { color: p.text, fontFamily: p.fontFamily }], children: article.title }),
4032
5231
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RichText, { html: article.body, palette: p, onLink: (u) => client.navigate(u) })
4033
5232
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.ScrollView, { contentContainerStyle: [styles5.content, { paddingBottom: bottomInset + 24 }], keyboardShouldPersistTaps: "handled", children: [
4034
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5233
+ modules.includes("search") ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4035
5234
  import_react_native6.TextInput,
4036
5235
  {
4037
5236
  value: query,
@@ -4043,26 +5242,11 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4043
5242
  returnKeyType: "search",
4044
5243
  style: [styles5.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }]
4045
5244
  }
4046
- ),
5245
+ ) : null,
4047
5246
  listed ? section(
4048
5247
  `Results for \u201C${query.trim()}\u201D`,
4049
5248
  listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => openArticle(a))) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.rowSub, { color: p.muted }], children: "No articles found." })
4050
- ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
4051
- recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => (close(), client.startGuide(g.id)), "\u25B6"))) : null,
4052
- checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => (close(), client.openChecklist(g.id)), "\u2611"))) : null,
4053
- sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => openArticle(a))))),
4054
- rc.faqs.length ? section(
4055
- "FAQ",
4056
- rc.faqs.map((f) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
4057
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.Pressable, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles5.faqHead, children: [
4058
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.rowTitle, styles5.flex, { color: p.text }], children: f.question }),
4059
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
4060
- ] }),
4061
- faq === f.id ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
4062
- ] }, f.id))
4063
- ) : null,
4064
- rc.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null
4065
- ] })
5249
+ ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: modules.filter((id) => id !== "search").map((id) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react4.Fragment, { children: renderModule(id) }, id)) })
4066
5250
  ] })
4067
5251
  ] }) });
4068
5252
  }
@@ -4082,7 +5266,10 @@ var styles5 = import_react_native6.StyleSheet.create({
4082
5266
  chev: { fontSize: 18 },
4083
5267
  faq: { flexDirection: "column", alignItems: "stretch" },
4084
5268
  faqHead: { flexDirection: "row", alignItems: "center", gap: 10 },
4085
- articleTitle: { fontSize: 22, fontWeight: "700" }
5269
+ articleTitle: { fontSize: 22, fontWeight: "700" },
5270
+ badge: { fontSize: 10, fontWeight: "800", letterSpacing: 0.5, paddingHorizontal: 6, paddingVertical: 2, borderRadius: 6, overflow: "hidden" },
5271
+ links: { flexDirection: "row", flexWrap: "wrap", gap: 16, marginTop: 4 },
5272
+ link: { fontSize: 14, fontWeight: "600" }
4086
5273
  });
4087
5274
 
4088
5275
  // src/ui/StepLayer.tsx
@@ -4128,7 +5315,10 @@ function positionCard(target, size, viewport, insets, placement, offset) {
4128
5315
 
4129
5316
  // src/ui/StepCard.tsx
4130
5317
  var import_react_native7 = require("react-native");
4131
- var import_jsx_runtime7 = require("react/jsx-runtime");
5318
+ var import_jsx_runtime7 = (
5319
+ // An empty description marks the image as decorative: screen readers skip it.
5320
+ require("react/jsx-runtime")
5321
+ );
4132
5322
  function StepCard({ view, client, palette: p, width, style, compact }) {
4133
5323
  const { step, guide } = view;
4134
5324
  const buttons = visibleButtons(step, view.canGoBack);
@@ -4136,7 +5326,16 @@ function StepCard({ view, client, palette: p, width, style, compact }) {
4136
5326
  const showProgress = step.showProgress && view.total > 1;
4137
5327
  const onLink = (url) => client.navigate(url);
4138
5328
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Surface, { palette: p, style: [{ width }, style], children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { accessible: false, accessibilityViewIsModal: view.asModal || step.type === "modal", style: compact ? styles6.compact : styles6.inner, children: [
4139
- step.imageUrl ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Image, { source: { uri: step.imageUrl }, style: [styles6.image, { borderRadius: Math.max(0, p.radius - 4) }], resizeMode: "cover", accessibilityIgnoresInvertColors: true }) : null,
5329
+ step.imageUrl ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
5330
+ import_react_native7.Image,
5331
+ {
5332
+ source: { uri: step.imageUrl },
5333
+ style: [styles6.image, { borderRadius: Math.max(0, p.radius - 4) }],
5334
+ resizeMode: "cover",
5335
+ accessibilityIgnoresInvertColors: true,
5336
+ ...step.imageAlt?.trim() ? { accessible: true, accessibilityRole: "image", accessibilityLabel: step.imageAlt.trim() } : { accessible: false, importantForAccessibility: "no" }
5337
+ }
5338
+ ) : null,
4140
5339
  step.title ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: styles6.header, children: [
4141
5340
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { accessibilityRole: "header", style: [styles6.title, { color: p.text, fontFamily: p.fontFamily }], children: step.title }),
4142
5341
  step.showClose ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CloseButton, { palette: p, onPress: () => client.closeStep(guide.id), label: "Dismiss guide" }) : null
@@ -4724,18 +5923,22 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
4724
5923
  const basePalette = palette(ui.theme, scheme);
4725
5924
  const messenger = ui.messenger ? client.getMessenger() : null;
4726
5925
  const showRcLauncher = !messenger && !!client.options.showResourceCenterLauncher && !!client.getResourceCenter();
5926
+ const unread = showRcLauncher ? client.getUnreadAnnouncements() : 0;
4727
5927
  const messengerLeft = messenger?.getSnapshot().config.launcher.position === "bottom-left";
4728
5928
  const checklistOnSameSide = ui.checklists.some((c) => c.guide.config.checklist?.position === "bottom-left" === messengerLeft) && client.options.showChecklistLauncher !== false;
4729
5929
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native10.View, { ref, collapsable: false, pointerEvents: "box-none", style: import_react_native10.StyleSheet.absoluteFill, onLayout: measureOrigin, children: [
4730
5930
  ui.checklists.map((c) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Checklist, { view: c, client, palette: palette(client.themeFor(c.guide), scheme), geometry, showLauncher: client.options.showChecklistLauncher !== false, launcherBottom: launcherOffset }, c.guide.id)),
4731
- showRcLauncher ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
5931
+ showRcLauncher ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
4732
5932
  import_react_native10.Pressable,
4733
5933
  {
4734
5934
  accessibilityRole: "button",
4735
- accessibilityLabel: client.getResourceCenter()?.launcher.label || "Help",
5935
+ accessibilityLabel: rcLauncherLabel(client),
4736
5936
  onPress: () => client.openResourceCenter(),
4737
5937
  style: [styles9.rc, client.getResourceCenter()?.launcher.position === "bottom-left" ? { left: insets.left + 16 } : { right: insets.right + 16 }, { bottom: insets.bottom + launcherOffset + (ui.checklists.length ? 56 : 0), backgroundColor: basePalette.primary }],
4738
- children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native10.Text, { style: [styles9.rcText, { color: basePalette.onPrimary }], children: "?" })
5938
+ children: [
5939
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native10.Text, { style: [styles9.rcText, { color: basePalette.onPrimary }], children: "?" }),
5940
+ unread ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native10.View, { pointerEvents: "none", style: [styles9.rcBadge, { borderColor: basePalette.primary }], children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native10.Text, { style: styles9.rcBadgeText, children: unread > 9 ? "9+" : unread }) }) : null
5941
+ ]
4739
5942
  }
4740
5943
  ) : null,
4741
5944
  messenger && client.options.showMessengerLauncher !== false ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MessengerLauncher, { store: messenger, palette: basePalette, style: [messengerLeft ? { left: insets.left + 16 } : { right: insets.right + 16 }, { bottom: insets.bottom + launcherOffset + (checklistOnSameSide ? 56 : 0) }] }) : null,
@@ -4746,28 +5949,76 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
4746
5949
  }
4747
5950
  var styles9 = import_react_native10.StyleSheet.create({
4748
5951
  rc: { position: "absolute", width: 48, height: 48, borderRadius: 24, alignItems: "center", justifyContent: "center", elevation: 8, shadowColor: "#000", shadowOpacity: 0.2, shadowRadius: 10, shadowOffset: { width: 0, height: 4 } },
4749
- rcText: { fontSize: 22, fontWeight: "800" }
5952
+ rcText: { fontSize: 22, fontWeight: "800" },
5953
+ rcBadge: { position: "absolute", top: -4, right: -4, minWidth: 20, height: 20, paddingHorizontal: 5, borderRadius: 10, borderWidth: 2, backgroundColor: "#dc2626", alignItems: "center", justifyContent: "center" },
5954
+ rcBadgeText: { color: "#ffffff", fontSize: 11, fontWeight: "800" }
4750
5955
  });
5956
+ function rcLauncherLabel(client) {
5957
+ const label = client.getResourceCenter()?.launcher.label || "Help";
5958
+ const unread = client.getUnreadAnnouncements();
5959
+ return unread ? `${label} (${unread} new update${unread === 1 ? "" : "s"})` : label;
5960
+ }
4751
5961
 
4752
- // src/ui/Target.tsx
4753
- var import_react8 = require("react");
4754
- var import_react_native11 = require("react-native");
5962
+ // src/ui/Autocapture.tsx
4755
5963
  var import_jsx_runtime11 = require("react/jsx-runtime");
4756
- var GuidelyContext = (0, import_react8.createContext)(null);
4757
- var GuidelyScreenContext = (0, import_react8.createContext)(null);
5964
+ function GuidelyMask({ children, ...rest }) {
5965
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native11.View, { ...rest, children });
5966
+ }
5967
+ var RESOLVE = {
5968
+ isIgnored: (type) => type === GuidelyOverlay,
5969
+ isMask: (type) => type === GuidelyMask
5970
+ };
5971
+ function useTapObserver(client) {
5972
+ const clientRef = (0, import_react8.useRef)(client);
5973
+ clientRef.current = client;
5974
+ return (0, import_react8.useMemo)(() => {
5975
+ const tracker = new TapTracker();
5976
+ const guard = (fn) => (e) => {
5977
+ try {
5978
+ fn(e);
5979
+ } catch {
5980
+ tracker.cancel();
5981
+ }
5982
+ };
5983
+ return {
5984
+ onTouchStartCapture: guard((e) => {
5985
+ const n = e.nativeEvent;
5986
+ if (!clientRef.current?.isTapCaptureOn()) return tracker.cancel();
5987
+ tracker.begin(n.pageX, n.pageY, Date.now(), Array.isArray(n.touches) ? n.touches.length : 1, n.identifier);
5988
+ }),
5989
+ onTouchMoveCapture: guard((e) => tracker.move(e.nativeEvent.pageX, e.nativeEvent.pageY, e.nativeEvent.identifier)),
5990
+ onTouchEndCapture: guard((e) => {
5991
+ const n = e.nativeEvent;
5992
+ const tap = tracker.end(n.pageX, n.pageY, Date.now(), n.identifier);
5993
+ const c = clientRef.current;
5994
+ if (!tap || !c) return;
5995
+ const r = resolveTapTarget(e, RESOLVE);
5996
+ if (!r.ignored) c.recordTap({ x: tap.x, y: tap.y, t: Date.now(), element: r.element, hit: r.hit });
5997
+ }),
5998
+ onTouchCancelCapture: guard(() => tracker.cancel())
5999
+ };
6000
+ }, []);
6001
+ }
6002
+
6003
+ // src/ui/Target.tsx
6004
+ var import_react9 = require("react");
6005
+ var import_react_native12 = require("react-native");
6006
+ var import_jsx_runtime12 = require("react/jsx-runtime");
6007
+ var GuidelyContext = (0, import_react9.createContext)(null);
6008
+ var GuidelyScreenContext = (0, import_react9.createContext)(null);
4758
6009
  function GuidelyScreenScope({ name, children }) {
4759
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(GuidelyScreenContext.Provider, { value: name, children });
6010
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(GuidelyScreenContext.Provider, { value: name, children });
4760
6011
  }
4761
6012
  function guidelyScreenLayout({ route, children }) {
4762
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(GuidelyScreenScope, { name: route.name, children });
6013
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(GuidelyScreenScope, { name: route.name, children });
4763
6014
  }
4764
- var TAP_SLOP = 10;
6015
+ var TAP_SLOP2 = 10;
4765
6016
  function useGuidelyTarget(id) {
4766
- const client = (0, import_react8.useContext)(GuidelyContext);
4767
- const screen = (0, import_react8.useContext)(GuidelyScreenContext);
4768
- const ref = (0, import_react8.useRef)(null);
4769
- const start = (0, import_react8.useRef)(null);
4770
- (0, import_react8.useEffect)(() => {
6017
+ const client = (0, import_react9.useContext)(GuidelyContext);
6018
+ const screen = (0, import_react9.useContext)(GuidelyScreenContext);
6019
+ const ref = (0, import_react9.useRef)(null);
6020
+ const start = (0, import_react9.useRef)(null);
6021
+ (0, import_react9.useEffect)(() => {
4771
6022
  if (!client || !id) return;
4772
6023
  return client.targets.register(id, {
4773
6024
  screen,
@@ -4778,15 +6029,15 @@ function useGuidelyTarget(id) {
4778
6029
  })
4779
6030
  });
4780
6031
  }, [client, id, screen]);
4781
- const onLayout = (0, import_react8.useCallback)(() => client?.targets.changed(), [client]);
4782
- const onTouchStart = (0, import_react8.useCallback)((e) => {
6032
+ const onLayout = (0, import_react9.useCallback)(() => client?.targets.changed(), [client]);
6033
+ const onTouchStart = (0, import_react9.useCallback)((e) => {
4783
6034
  start.current = { x: e.nativeEvent.pageX, y: e.nativeEvent.pageY };
4784
6035
  }, []);
4785
- const onTouchEnd = (0, import_react8.useCallback)(
6036
+ const onTouchEnd = (0, import_react9.useCallback)(
4786
6037
  (e) => {
4787
6038
  const s = start.current;
4788
6039
  start.current = null;
4789
- if (s && Math.abs(e.nativeEvent.pageX - s.x) < TAP_SLOP && Math.abs(e.nativeEvent.pageY - s.y) < TAP_SLOP) client?.targets.tapped(id);
6040
+ if (s && Math.abs(e.nativeEvent.pageX - s.x) < TAP_SLOP2 && Math.abs(e.nativeEvent.pageY - s.y) < TAP_SLOP2) client?.targets.tapped(id);
4790
6041
  },
4791
6042
  [client, id]
4792
6043
  );
@@ -4794,8 +6045,8 @@ function useGuidelyTarget(id) {
4794
6045
  }
4795
6046
  function GuidelyTarget({ id, children, onLayout, onTouchStart, onTouchEnd, ...rest }) {
4796
6047
  const t = useGuidelyTarget(id);
4797
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
4798
- import_react_native11.View,
6048
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
6049
+ import_react_native12.View,
4799
6050
  {
4800
6051
  ...rest,
4801
6052
  ref: t.ref,
@@ -4819,7 +6070,7 @@ function GuidelyTarget({ id, children, onLayout, onTouchStart, onTouchEnd, ...re
4819
6070
 
4820
6071
  // src/index.tsx
4821
6072
  var import_flags6 = require("@guidelytech/flags");
4822
- var import_jsx_runtime12 = require("react/jsx-runtime");
6073
+ var import_jsx_runtime13 = require("react/jsx-runtime");
4823
6074
  function deviceLocale() {
4824
6075
  try {
4825
6076
  return Intl.DateTimeFormat().resolvedOptions().locale;
@@ -4828,33 +6079,34 @@ function deviceLocale() {
4828
6079
  }
4829
6080
  }
4830
6081
  function defaultBridge(appVersion) {
4831
- const { width, height } = import_react_native12.Dimensions.get("window");
6082
+ const { width, height } = import_react_native13.Dimensions.get("window");
4832
6083
  return {
4833
6084
  device: {
4834
- platform: import_react_native12.Platform.OS === "android" ? "android" : "ios",
4835
- deviceType: import_react_native12.Platform.OS === "ios" && import_react_native12.Platform.isPad || Math.min(width, height) >= 600 ? "tablet" : "mobile",
6085
+ platform: import_react_native13.Platform.OS === "android" ? "android" : "ios",
6086
+ deviceType: import_react_native13.Platform.OS === "ios" && import_react_native13.Platform.isPad || Math.min(width, height) >= 600 ? "tablet" : "mobile",
4836
6087
  locale: deviceLocale(),
4837
6088
  appVersion
4838
6089
  },
4839
- openExternal: (url) => void import_react_native12.Linking.openURL(url).catch(() => {
4840
- })
6090
+ openExternal: (url) => void import_react_native13.Linking.openURL(url).catch(() => {
6091
+ }),
6092
+ windowSize: () => import_react_native13.Dimensions.get("window")
4841
6093
  };
4842
6094
  }
4843
6095
  function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPayload, flagAttributes, ...options }) {
4844
- const [client, setClient] = (0, import_react9.useState)(null);
4845
- const optionsRef = (0, import_react9.useRef)(options);
6096
+ const [client, setClient] = (0, import_react10.useState)(null);
6097
+ const optionsRef = (0, import_react10.useRef)(options);
4846
6098
  optionsRef.current = options;
4847
- const userRef = (0, import_react9.useRef)(user);
6099
+ const userRef = (0, import_react10.useRef)(user);
4848
6100
  userRef.current = user;
4849
6101
  const flagsOpts = options.flags === false ? null : options.flags ?? {};
4850
6102
  const payload = flagsPayload ?? flagsOpts?.payload ?? null;
4851
- const payloadRef = (0, import_react9.useRef)(payload);
6103
+ const payloadRef = (0, import_react10.useRef)(payload);
4852
6104
  payloadRef.current = payload;
4853
- const appAttributes = (0, import_react9.useMemo)(() => ({ ...flagsOpts?.attributes ?? {}, ...flagAttributes ?? {} }), [(0, import_flags3.stableStringify)(flagsOpts?.attributes ?? {}), (0, import_flags3.stableStringify)(flagAttributes ?? {})]);
4854
- const appAttributesRef = (0, import_react9.useRef)(appAttributes);
6105
+ const appAttributes = (0, import_react10.useMemo)(() => ({ ...flagsOpts?.attributes ?? {}, ...flagAttributes ?? {} }), [(0, import_flags3.stableStringify)(flagsOpts?.attributes ?? {}), (0, import_flags3.stableStringify)(flagAttributes ?? {})]);
6106
+ const appAttributesRef = (0, import_react10.useRef)(appAttributes);
4855
6107
  appAttributesRef.current = appAttributes;
4856
6108
  const { projectId, environment, apiUrl, storage } = options;
4857
- (0, import_react9.useEffect)(() => {
6109
+ (0, import_react10.useEffect)(() => {
4858
6110
  if (!projectId) return;
4859
6111
  let c;
4860
6112
  try {
@@ -4880,7 +6132,7 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
4880
6132
  }
4881
6133
  setClient(c);
4882
6134
  void c.init();
4883
- const sub = import_react_native12.AppState.addEventListener("change", (s) => c.setAppState(s));
6135
+ const sub = import_react_native13.AppState.addEventListener("change", (s) => c.setAppState(s));
4884
6136
  return () => {
4885
6137
  sub.remove();
4886
6138
  c.destroy();
@@ -4888,8 +6140,8 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
4888
6140
  };
4889
6141
  }, [projectId, environment, apiUrl, storage]);
4890
6142
  const userKey = user ? JSON.stringify([user.id, user.traits ?? null, user.account ?? null]) : "";
4891
- const hadUser = (0, import_react9.useRef)(false);
4892
- (0, import_react9.useEffect)(() => {
6143
+ const hadUser = (0, import_react10.useRef)(false);
6144
+ (0, import_react10.useEffect)(() => {
4893
6145
  if (!client) return;
4894
6146
  if (user && user.id) {
4895
6147
  hadUser.current = true;
@@ -4899,16 +6151,21 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
4899
6151
  client.reset();
4900
6152
  }
4901
6153
  }, [client, userKey]);
4902
- (0, import_react9.useEffect)(() => {
6154
+ (0, import_react10.useEffect)(() => {
4903
6155
  client?.flags.setAttributes(appAttributes);
4904
6156
  }, [client, appAttributes]);
6157
+ (0, import_react10.useEffect)(() => {
6158
+ client?.setLocale(options.locale ?? null);
6159
+ }, [client, options.locale]);
4905
6160
  const bootstrap = useBootstrap(payload, { ...user?.id ? { ...user.traits ?? {}, id: user.id } : {}, ...appAttributes });
4906
- const flags = (0, import_react9.useMemo)(() => ({ client: client?.flags ?? null, bootstrap }), [client, bootstrap]);
4907
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(GuidelyContext.Provider, { value: client, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(FlagsContext.Provider, { value: flags, children: [
4908
- children,
4909
- client ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(GuidelyOverlay, { client, insets, launcherOffset }) : null
6161
+ const flags = (0, import_react10.useMemo)(() => ({ client: client?.flags ?? null, bootstrap }), [client, bootstrap]);
6162
+ const tapObserver = useTapObserver(client);
6163
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(GuidelyContext.Provider, { value: client, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(FlagsContext.Provider, { value: flags, children: [
6164
+ options.autocapture === false ? children : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native13.View, { style: styles10.root, ...tapObserver, children }),
6165
+ client ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(GuidelyOverlay, { client, insets, launcherOffset }) : null
4910
6166
  ] }) });
4911
6167
  }
6168
+ var styles10 = import_react_native13.StyleSheet.create({ root: { flex: 1 } });
4912
6169
  var NOOP_RESULTS = {
4913
6170
  init: Promise.resolve(),
4914
6171
  ready: Promise.resolve(),
@@ -4957,12 +6214,12 @@ var noopClient = new Proxy({}, {
4957
6214
  }
4958
6215
  });
4959
6216
  function useGuidely() {
4960
- return (0, import_react9.useContext)(GuidelyContext) ?? noopClient;
6217
+ return (0, import_react10.useContext)(GuidelyContext) ?? noopClient;
4961
6218
  }
4962
6219
  function useGuidelyReady() {
4963
- const client = (0, import_react9.useContext)(GuidelyContext);
4964
- const [ready, setReady] = (0, import_react9.useState)(() => !!client?.isClientReady());
4965
- (0, import_react9.useEffect)(() => {
6220
+ const client = (0, import_react10.useContext)(GuidelyContext);
6221
+ const [ready, setReady] = (0, import_react10.useState)(() => !!client?.isClientReady());
6222
+ (0, import_react10.useEffect)(() => {
4966
6223
  if (!client) return setReady(false);
4967
6224
  setReady(client.isClientReady());
4968
6225
  const off = client.on("ready", () => setReady(true));
@@ -4972,44 +6229,44 @@ function useGuidelyReady() {
4972
6229
  return ready;
4973
6230
  }
4974
6231
  function useGuidelyIdentify(userId, traits, account) {
4975
- const client = (0, import_react9.useContext)(GuidelyContext);
6232
+ const client = (0, import_react10.useContext)(GuidelyContext);
4976
6233
  const key = JSON.stringify([userId ?? null, traits ?? null, account ?? null]);
4977
- (0, import_react9.useEffect)(() => {
6234
+ (0, import_react10.useEffect)(() => {
4978
6235
  if (client && userId) void client.identify(userId, traits, account);
4979
6236
  }, [client, key]);
4980
6237
  }
4981
6238
  function useGuidelyEvent(event, handler) {
4982
- const client = (0, import_react9.useContext)(GuidelyContext);
4983
- const ref = (0, import_react9.useRef)(handler);
6239
+ const client = (0, import_react10.useContext)(GuidelyContext);
6240
+ const ref = (0, import_react10.useRef)(handler);
4984
6241
  ref.current = handler;
4985
- (0, import_react9.useEffect)(() => {
6242
+ (0, import_react10.useEffect)(() => {
4986
6243
  if (!client) return;
4987
6244
  return client.on(event, ((...args) => ref.current(...args)));
4988
6245
  }, [client, event]);
4989
6246
  }
4990
6247
  function useGuidelyScreen(name, properties) {
4991
- const client = (0, import_react9.useContext)(GuidelyContext);
4992
- (0, import_react9.useEffect)(() => {
6248
+ const client = (0, import_react10.useContext)(GuidelyContext);
6249
+ (0, import_react10.useEffect)(() => {
4993
6250
  if (client && name) client.screen(name, properties);
4994
6251
  }, [client, name]);
4995
6252
  }
4996
6253
  function useGuidelyNavigation(navigationRef, options = {}) {
4997
- const client = (0, import_react9.useContext)(GuidelyContext);
4998
- const nameRef = (0, import_react9.useRef)(options.screenName);
6254
+ const client = (0, import_react10.useContext)(GuidelyContext);
6255
+ const nameRef = (0, import_react10.useRef)(options.screenName);
4999
6256
  nameRef.current = options.screenName;
5000
6257
  const report = () => {
5001
6258
  const route = navigationRef.getCurrentRoute?.();
5002
6259
  if (client && route?.name) client.screen(nameRef.current ? nameRef.current(route) : route.name);
5003
6260
  };
5004
- (0, import_react9.useEffect)(report, [client]);
6261
+ (0, import_react10.useEffect)(report, [client]);
5005
6262
  return { onReady: report, onStateChange: report };
5006
6263
  }
5007
6264
  function useUiSelector(select, fallback) {
5008
- const client = (0, import_react9.useContext)(GuidelyContext);
5009
- const selectRef = (0, import_react9.useRef)(select);
6265
+ const client = (0, import_react10.useContext)(GuidelyContext);
6266
+ const selectRef = (0, import_react10.useRef)(select);
5010
6267
  selectRef.current = select;
5011
- const [value, setValue] = (0, import_react9.useState)(() => client ? select(client) : fallback);
5012
- (0, import_react9.useEffect)(() => {
6268
+ const [value, setValue] = (0, import_react10.useState)(() => client ? select(client) : fallback);
6269
+ (0, import_react10.useEffect)(() => {
5013
6270
  if (!client) return;
5014
6271
  let last = JSON.stringify(selectRef.current(client));
5015
6272
  setValue(selectRef.current(client));
@@ -5049,14 +6306,18 @@ function useActiveGuide() {
5049
6306
  FeatureValue,
5050
6307
  FlagsProvider,
5051
6308
  GuidelyClient,
6309
+ GuidelyMask,
5052
6310
  GuidelyOverlay,
5053
6311
  GuidelyProvider,
5054
6312
  GuidelyScreenScope,
5055
6313
  GuidelyTarget,
6314
+ HeatmapBuffer,
5056
6315
  InMemoryStickyBucketStore,
5057
6316
  Messenger,
6317
+ RageClickDetector,
5058
6318
  SDK_NAME,
5059
6319
  SDK_VERSION,
6320
+ TapTracker,
5060
6321
  TargetRegistry,
5061
6322
  createFlagsClient,
5062
6323
  createSseParser,