@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.js CHANGED
@@ -3,9 +3,9 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
 
5
5
  // src/index.tsx
6
- import { useContext as useContext3, useEffect as useEffect9, useMemo as useMemo5, useRef as useRef6, useState as useState8 } from "react";
7
- import { AppState as AppState2, Dimensions, Linking, Platform as Platform6 } from "react-native";
8
- import { createStaticFlags as createStaticFlags2, stableStringify as stableStringify2 } from "@guidelytech/flags";
6
+ import { useContext as useContext3, useEffect as useEffect9, useMemo as useMemo6, useRef as useRef7, useState as useState8 } from "react";
7
+ import { AppState as AppState2, Dimensions, Linking, Platform as Platform6, StyleSheet as StyleSheet10, View as View12 } from "react-native";
8
+ import { createStaticFlags as createStaticFlags2, stableStringify as stableStringify3 } from "@guidelytech/flags";
9
9
 
10
10
  // src/flags.tsx
11
11
  import { createFlagsClient, createStaticFlags, jsonEquals, stableStringify } from "@guidelytech/flags";
@@ -96,7 +96,7 @@ function FlagsProvider({ children, client: external, payload, ...options }) {
96
96
  return /* @__PURE__ */ jsx(FlagsContext.Provider, { value, children });
97
97
  }
98
98
 
99
- // ../shared/dist/chunk-UW6NRYRY.js
99
+ // ../shared/dist/chunk-TGIUEYXM.js
100
100
  function fnv1a(input) {
101
101
  let h = 2166136261;
102
102
  for (let i = 0; i < input.length; i++) {
@@ -276,6 +276,8 @@ function resolveField(rule, ctx) {
276
276
  return ctx.now.getHours();
277
277
  case "rollout.percentage":
278
278
  return rolloutBucket(ctx.userId || ctx.anonymousId, ctx.rolloutSalt || "guidely");
279
+ case "experiment.bucket":
280
+ return rule.key ? rolloutBucket(ctx.userId || ctx.anonymousId, rule.key) : void 0;
279
281
  case "environment":
280
282
  return ctx.environment;
281
283
  case "locale":
@@ -286,6 +288,9 @@ function resolveField(rule, ctx) {
286
288
  return ctx.screen;
287
289
  case "app.version":
288
290
  return ctx.appVersion;
291
+ case "page.views":
292
+ case "feature.clicks":
293
+ return rule.key ? ctx.tagUsage?.[rule.key] ?? 0 : 0;
289
294
  default:
290
295
  return void 0;
291
296
  }
@@ -394,8 +399,8 @@ function sanitizeProperties(props) {
394
399
  for (const [k, v] of Object.entries(props)) {
395
400
  if (SENSITIVE_PROPERTY_PATTERN.test(k)) continue;
396
401
  if (typeof v === "function" || typeof v === "symbol") continue;
397
- const str = typeof v === "string" ? v : JSON.stringify(v ?? null);
398
- size += k.length + (str?.length ?? 0);
402
+ const str2 = typeof v === "string" ? v : JSON.stringify(v ?? null);
403
+ size += k.length + (str2?.length ?? 0);
399
404
  if (size > MAX_PROPERTY_BYTES) break;
400
405
  out[k.slice(0, 100)] = typeof v === "string" ? v.slice(0, 1e3) : v;
401
406
  }
@@ -552,53 +557,252 @@ function isArticleListed(collections, article, member) {
552
557
  }
553
558
  return true;
554
559
  }
560
+ var RC_MODULE_IDS = ["announcements", "search", "checklists", "guides", "articles", "links", "faqs"];
561
+ function orderedModules(modules) {
562
+ const known = (modules ?? []).filter((m, i, all) => RC_MODULE_IDS.includes(m.id) && all.findIndex((x) => x.id === m.id) === i);
563
+ const missing2 = RC_MODULE_IDS.filter((id) => !known.some((m) => m.id === id)).map((id) => ({ id, enabled: true }));
564
+ return [...known, ...missing2];
565
+ }
566
+ function visibleAnnouncements(items, member, now = Date.now()) {
567
+ return (items ?? []).filter((a) => {
568
+ const t = Date.parse(a.publishedAt);
569
+ return !Number.isNaN(t) && t <= now && matchesAudiences(a.audienceIds, member);
570
+ }).sort((a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt));
571
+ }
572
+ function unreadAnnouncements(items, lastSeenAt) {
573
+ const seen = lastSeenAt ? Date.parse(lastSeenAt) : NaN;
574
+ if (Number.isNaN(seen)) return items.length;
575
+ return items.filter((a) => Date.parse(a.publishedAt) > seen).length;
576
+ }
555
577
  var CAPTURE_SCREENSHOT_MAX_BYTES = 8 * 1024 * 1024;
556
-
557
- // ../sdk-core/dist/chunk-CYAG6ZYX.js
558
- function readJson(store, key, fallback) {
559
- const raw = store.get(key);
560
- if (raw == null) return fallback;
561
- try {
562
- return JSON.parse(raw);
563
- } catch {
564
- return fallback;
565
- }
578
+ var DEFAULT_LOCALE = "en";
579
+ function normalizeLocale(input) {
580
+ if (typeof input !== "string") return "";
581
+ const parts = input.trim().replace(/_/g, "-").split("-").filter(Boolean);
582
+ if (!parts.length || !/^[a-zA-Z]{2,3}$/.test(parts[0])) return "";
583
+ 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("-");
584
+ }
585
+ function baseLanguage(locale) {
586
+ return normalizeLocale(locale).split("-")[0] ?? "";
587
+ }
588
+ function resolveLocale(requested, available, defaultLocale = DEFAULT_LOCALE) {
589
+ const def = normalizeLocale(defaultLocale) || DEFAULT_LOCALE;
590
+ const avail = available.map(normalizeLocale).filter(Boolean);
591
+ const all = [def, ...avail.filter((a) => a.toLowerCase() !== def.toLowerCase())];
592
+ for (const raw of requested) {
593
+ const want = normalizeLocale(raw);
594
+ if (!want) continue;
595
+ const exact = all.find((a) => a.toLowerCase() === want.toLowerCase());
596
+ if (exact) return exact;
597
+ const base = baseLanguage(want);
598
+ const sameBase = all.find((a) => a.toLowerCase() === base) ?? all.find((a) => baseLanguage(a) === base);
599
+ if (sameBase) return sameBase;
600
+ }
601
+ return def;
602
+ }
603
+ function visitorLocales(explicit, traits, platform = []) {
604
+ const out = [];
605
+ const push = (v) => {
606
+ const n = normalizeLocale(v);
607
+ if (n && !out.includes(n)) out.push(n);
608
+ };
609
+ push(explicit);
610
+ push(traits?.language);
611
+ push(traits?.locale);
612
+ for (const p of platform) push(p);
613
+ return out;
566
614
  }
567
- function writeJson(store, key, value) {
568
- try {
569
- store.set(key, JSON.stringify(value));
570
- } catch {
571
- }
615
+ function guideLocales(config) {
616
+ const t = config.localization?.translations ?? {};
617
+ return Object.keys(t).filter((l) => Object.values(t[l] ?? {}).some((v) => typeof v === "string" && v.trim() !== ""));
618
+ }
619
+ function pick(map, key, fallback) {
620
+ const v = map?.[key];
621
+ return typeof v === "string" && v.trim() !== "" ? v : fallback;
622
+ }
623
+ function applyGuideTranslations(config, map) {
624
+ if (!map || !Object.keys(map).length) return config;
625
+ return {
626
+ ...config,
627
+ steps: config.steps.map((s) => ({
628
+ ...s,
629
+ title: pick(map, `steps.${s.id}.title`, s.title),
630
+ body: pick(map, `steps.${s.id}.body`, s.body),
631
+ customHtml: pick(map, `steps.${s.id}.customHtml`, s.customHtml),
632
+ imageAlt: pick(map, `steps.${s.id}.imageAlt`, s.imageAlt),
633
+ buttons: s.buttons.map((b) => ({ ...b, label: pick(map, `steps.${s.id}.buttons.${b.id}.label`, b.label) }))
634
+ })),
635
+ checklist: config.checklist ? {
636
+ ...config.checklist,
637
+ launcherLabel: pick(map, "checklist.launcherLabel", config.checklist.launcherLabel),
638
+ completionTitle: pick(map, "checklist.completionTitle", config.checklist.completionTitle),
639
+ completionBody: pick(map, "checklist.completionBody", config.checklist.completionBody),
640
+ 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) }))
641
+ } : config.checklist,
642
+ survey: config.survey ? {
643
+ ...config.survey,
644
+ thankYouTitle: pick(map, "survey.thankYouTitle", config.survey.thankYouTitle),
645
+ thankYouBody: pick(map, "survey.thankYouBody", config.survey.thankYouBody),
646
+ questions: config.survey.questions.map((q) => ({
647
+ ...q,
648
+ title: pick(map, `survey.questions.${q.id}.title`, q.title),
649
+ description: pick(map, `survey.questions.${q.id}.description`, q.description),
650
+ placeholder: pick(map, `survey.questions.${q.id}.placeholder`, q.placeholder),
651
+ lowLabel: pick(map, `survey.questions.${q.id}.lowLabel`, q.lowLabel),
652
+ highLabel: pick(map, `survey.questions.${q.id}.highLabel`, q.highLabel),
653
+ options: q.options.map((o) => ({ ...o, label: pick(map, `survey.questions.${q.id}.options.${o.id}.label`, o.label) }))
654
+ }))
655
+ } : config.survey
656
+ };
657
+ }
658
+ function localizeGuide(guide, requested, defaultLocale = DEFAULT_LOCALE) {
659
+ const translations = guide.config.localization?.translations ?? {};
660
+ const locale = resolveLocale(requested, guideLocales(guide.config), defaultLocale);
661
+ const key = Object.keys(translations).find((l) => normalizeLocale(l).toLowerCase() === locale.toLowerCase());
662
+ if (!key) return { ...guide, locale };
663
+ return { ...guide, locale, config: applyGuideTranslations(guide.config, translations[key]) };
664
+ }
665
+ function resourceCenterLocales(rc) {
666
+ const set = new Set(Object.keys(rc.translations ?? {}));
667
+ for (const a of rc.articles) for (const l of Object.keys(a.translations ?? {})) set.add(l);
668
+ for (const a of rc.announcements ?? []) for (const l of Object.keys(a.translations ?? {})) set.add(l);
669
+ return [...set];
670
+ }
671
+ function translationFor(map, locale) {
672
+ if (!map) return void 0;
673
+ const key = Object.keys(map).find((l) => normalizeLocale(l).toLowerCase() === locale.toLowerCase());
674
+ return key ? map[key] : void 0;
675
+ }
676
+ function localizeArticle(a, locale) {
677
+ const t = translationFor(a.translations, locale);
678
+ if (!t) return a;
679
+ const body = t.body?.trim() ? t.body : a.body;
680
+ return { ...a, title: t.title?.trim() ? t.title : a.title, summary: t.summary?.trim() ? t.summary : a.summary, body, ...t.bodyOmitted ? { bodyOmitted: true } : {} };
681
+ }
682
+ function localizeAnnouncement(a, locale) {
683
+ const t = translationFor(a.translations, locale);
684
+ if (!t) return a;
685
+ return { ...a, title: t.title?.trim() ? t.title : a.title, body: t.body?.trim() ? t.body : a.body };
686
+ }
687
+ function localizeResourceCenter(rc, requested, defaultLocale = DEFAULT_LOCALE) {
688
+ const locale = resolveLocale(requested, resourceCenterLocales(rc), defaultLocale);
689
+ if (locale.toLowerCase() === normalizeLocale(defaultLocale).toLowerCase()) return { ...rc, locale };
690
+ const map = translationFor(rc.translations, locale);
691
+ return {
692
+ ...rc,
693
+ locale,
694
+ title: pick(map, "title", rc.title),
695
+ launcher: { ...rc.launcher, label: pick(map, "launcher.label", rc.launcher.label) },
696
+ collections: rc.collections.map((c) => ({ ...c, name: pick(map, `collections.${c.id}.name`, c.name), description: pick(map, `collections.${c.id}.description`, c.description) })),
697
+ links: rc.links.map((l) => ({ ...l, label: pick(map, `links.${l.id}.label`, l.label) })),
698
+ faqs: rc.faqs.map((f) => ({ ...f, question: pick(map, `faqs.${f.id}.question`, f.question), answer: pick(map, `faqs.${f.id}.answer`, f.answer) })),
699
+ articles: rc.articles.map((a) => localizeArticle(a, locale)),
700
+ announcements: rc.announcements?.map((a) => localizeAnnouncement(a, locale))
701
+ };
572
702
  }
573
- var KEYS = {
574
- anon: "guidely:anon",
575
- seen: "guidely:seen",
576
- config: (key) => `guidely:config:${key}`,
577
- etag: (key) => `guidely:etag:${key}`,
578
- state: (key) => `guidely:state:${key}`,
579
- user: (key) => `guidely:user:${key}`,
580
- queue: (key) => `guidely:queue:${key}`,
581
- session: "guidely:session",
582
- active: "guidely:active",
583
- authoring: "guidely:authoring",
584
- /** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
585
- capture: "guidely:capture",
586
- capturePending: "guidely:capture:pending",
587
- /** Messenger conversations this browser can read: [{ id, token }]. */
588
- conversations: (key) => `guidely:conversations:${key}`,
589
- /** Announcement ids opened in the messenger's News tab. */
590
- newsSeen: (key) => `guidely:news:${key}`
591
- };
592
703
 
593
- // ../sdk-core/dist/chunk-AKNE7D5M.js
704
+ // ../sdk-core/dist/chunk-JUSM7J5N.js
594
705
  var __defProp2 = Object.defineProperty;
595
706
  var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
596
707
  var __publicField2 = (obj, key, value) => __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
708
+ var MAX_RETRY_AFTER_MS = 60 * 60 * 1e3;
709
+ var OFFLINE_WAIT_MS = 3e4;
710
+ var ServerHealth = class {
711
+ constructor(opts = {}) {
712
+ __publicField2(this, "failures", 0);
713
+ __publicField2(this, "openUntil", 0);
714
+ __publicField2(this, "lastOkAt", 0);
715
+ __publicField2(this, "listeners", /* @__PURE__ */ new Set());
716
+ __publicField2(this, "threshold");
717
+ __publicField2(this, "baseMs");
718
+ __publicField2(this, "maxMs");
719
+ __publicField2(this, "now");
720
+ __publicField2(this, "random");
721
+ this.threshold = opts.threshold ?? 3;
722
+ this.baseMs = opts.baseMs ?? 5e3;
723
+ this.maxMs = opts.maxMs ?? 5 * 60 * 1e3;
724
+ this.now = opts.now ?? Date.now;
725
+ this.random = opts.random ?? Math.random;
726
+ }
727
+ /** The server answered (2xx, 3xx, or a 4xx other than 429): it is up. */
728
+ success() {
729
+ const wasDown = this.failures > 0 || this.openUntil > 0;
730
+ this.failures = 0;
731
+ this.openUntil = 0;
732
+ this.lastOkAt = this.now();
733
+ if (wasDown) this.notify();
734
+ }
735
+ /** The request failed or the server is overloaded. `retryAfterMs` comes from a `Retry-After` header. */
736
+ failure(retryAfterMs) {
737
+ this.failures += 1;
738
+ const now = this.now();
739
+ if (retryAfterMs != null && retryAfterMs > 0) {
740
+ this.openUntil = Math.max(this.openUntil, now + Math.min(retryAfterMs, MAX_RETRY_AFTER_MS));
741
+ return;
742
+ }
743
+ if (this.failures < this.threshold) return;
744
+ const cooldown = Math.min(this.maxMs, this.baseMs * 2 ** (this.failures - this.threshold));
745
+ this.openUntil = Math.max(this.openUntil, now + cooldown * (0.8 + this.random() * 0.4));
746
+ }
747
+ /** Milliseconds background traffic should wait before trying again (0: send now). */
748
+ waitMs() {
749
+ if (typeof navigator !== "undefined" && navigator.onLine === false) return OFFLINE_WAIT_MS;
750
+ return Math.max(0, this.openUntil - this.now());
751
+ }
752
+ canSend() {
753
+ return this.waitMs() === 0;
754
+ }
755
+ /**
756
+ * The last request got an answer. Unload beacons can't report back, so what they carry is only
757
+ * forgotten while this holds; otherwise it stays queued and is sent again (the server drops duplicates).
758
+ */
759
+ get reachable() {
760
+ return this.failures === 0 && this.lastOkAt > 0;
761
+ }
762
+ /** The browser came back online: end the cooldown and let queued traffic try straight away. */
763
+ online() {
764
+ this.openUntil = 0;
765
+ this.notify();
766
+ }
767
+ /** Called when the server answers again after failures, or the browser comes back online. */
768
+ onRecover(cb) {
769
+ this.listeners.add(cb);
770
+ return () => this.listeners.delete(cb);
771
+ }
772
+ notify() {
773
+ for (const cb of Array.from(this.listeners)) {
774
+ try {
775
+ cb();
776
+ } catch {
777
+ }
778
+ }
779
+ }
780
+ };
781
+ function parseRetryAfter(value, now = Date.now()) {
782
+ if (!value) return null;
783
+ const trimmed = value.trim();
784
+ if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
785
+ const at = Date.parse(trimmed);
786
+ return Number.isNaN(at) ? null : Math.max(0, at - now);
787
+ }
788
+ function isServerFailure(status) {
789
+ return status === 429 || status >= 500;
790
+ }
791
+ var STREAM_RESPONSE_TIMEOUT_MS = 3e4;
792
+ var STREAM_IDLE_TIMEOUT_MS = 6e4;
597
793
  var Api = class {
598
- constructor(baseUrl, key, fetchImpl) {
794
+ constructor(baseUrl, key, fetchImpl, health) {
599
795
  __publicField2(this, "baseUrl", baseUrl);
600
796
  __publicField2(this, "key", key);
601
797
  __publicField2(this, "fetchImpl", fetchImpl);
798
+ __publicField2(this, "health");
799
+ this.health = health ?? new ServerHealth();
800
+ }
801
+ /** Feeds an answer (or a failed request, `null`) into {@link health}. */
802
+ observe(res) {
803
+ if (!res) this.health.failure();
804
+ else if (isServerFailure(res.status)) this.health.failure(parseRetryAfter(res.headers?.get?.("retry-after")));
805
+ else this.health.success();
602
806
  }
603
807
  url(path, params = {}) {
604
808
  const query = Object.entries({ key: this.key, ...params }).filter((e) => e[1] != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
@@ -618,15 +822,22 @@ var Api = class {
618
822
  const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
619
823
  const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
620
824
  try {
621
- const res = await f(this.url(path, options.params), {
622
- method,
623
- headers,
624
- body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
625
- keepalive: options.keepalive,
626
- signal: ac?.signal,
627
- credentials: "omit",
628
- mode: "cors"
629
- });
825
+ let res;
826
+ try {
827
+ res = await f(this.url(path, options.params), {
828
+ method,
829
+ headers,
830
+ body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
831
+ keepalive: options.keepalive,
832
+ signal: ac?.signal,
833
+ credentials: "omit",
834
+ mode: "cors"
835
+ });
836
+ } catch (err) {
837
+ this.observe(null);
838
+ throw err;
839
+ }
840
+ this.observe(res);
630
841
  let data = null;
631
842
  if (res.status !== 304 && res.status !== 204) {
632
843
  try {
@@ -652,30 +863,53 @@ var Api = class {
652
863
  }
653
864
  /**
654
865
  * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
655
- * returned instead. No timeout: Scout replies can take a while and the server keeps the stream alive.
866
+ * returned instead. Scout replies can take a while, so there is no overall timeout: the response
867
+ * must start within 30 s and then never go quiet for more than 60 s (the server sends keepalives),
868
+ * otherwise the request is aborted and rejects.
656
869
  */
657
870
  async stream(path, body, onEvent, headers = {}) {
658
871
  const f = this.fetcher;
659
872
  if (!f) throw new Error("fetch is not available");
660
- const res = await f(this.url(path), {
661
- method: "POST",
662
- headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
663
- body: JSON.stringify(body),
664
- credentials: "omit",
665
- mode: "cors"
666
- });
667
- const type = res.headers?.get?.("content-type") ?? "";
668
- if (!type.includes("text/event-stream") || !res.body) {
669
- let data = null;
873
+ const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
874
+ let timer = null;
875
+ const arm = (ms) => {
876
+ if (!ac) return;
877
+ if (timer) clearTimeout(timer);
878
+ timer = setTimeout(() => ac.abort(), ms);
879
+ };
880
+ arm(STREAM_RESPONSE_TIMEOUT_MS);
881
+ try {
882
+ let res;
670
883
  try {
671
- data = await res.json();
672
- } catch {
673
- data = null;
884
+ res = await f(this.url(path), {
885
+ method: "POST",
886
+ headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
887
+ body: JSON.stringify(body),
888
+ signal: ac?.signal,
889
+ credentials: "omit",
890
+ mode: "cors"
891
+ });
892
+ } catch (err) {
893
+ this.observe(null);
894
+ throw err;
895
+ }
896
+ this.observe(res);
897
+ const type = res.headers?.get?.("content-type") ?? "";
898
+ if (!type.includes("text/event-stream") || !res.body) {
899
+ let data = null;
900
+ try {
901
+ data = await res.json();
902
+ } catch {
903
+ data = null;
904
+ }
905
+ return { status: res.status, ok: res.ok, data, etag: null };
674
906
  }
675
- return { status: res.status, ok: res.ok, data, etag: null };
907
+ arm(STREAM_IDLE_TIMEOUT_MS);
908
+ await readEventStream(res.body, onEvent, () => arm(STREAM_IDLE_TIMEOUT_MS));
909
+ return { status: res.status, ok: res.ok, data: null, etag: null };
910
+ } finally {
911
+ if (timer) clearTimeout(timer);
676
912
  }
677
- await readEventStream(res.body, onEvent);
678
- return { status: res.status, ok: res.ok, data: null, etag: null };
679
913
  }
680
914
  /** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
681
915
  beacon(path, body) {
@@ -706,7 +940,7 @@ async function withRetry(send, attempts = 3, baseMs = 500) {
706
940
  function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
707
941
  return withRetry(() => api.post(path, body), attempts, baseMs);
708
942
  }
709
- async function readEventStream(body, onEvent) {
943
+ async function readEventStream(body, onEvent, onChunk) {
710
944
  const reader = body.getReader();
711
945
  const decoder = new TextDecoder();
712
946
  let buffer = "";
@@ -729,6 +963,7 @@ async function readEventStream(body, onEvent) {
729
963
  };
730
964
  for (; ; ) {
731
965
  const { done, value } = await reader.read();
966
+ if (value) onChunk?.();
732
967
  if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
733
968
  let sep;
734
969
  while ((sep = buffer.indexOf("\n\n")) >= 0) {
@@ -739,8 +974,46 @@ async function readEventStream(body, onEvent) {
739
974
  }
740
975
  if (buffer.trim()) dispatch(buffer);
741
976
  }
977
+ function readJson(store, key, fallback) {
978
+ const raw = store.get(key);
979
+ if (raw == null) return fallback;
980
+ try {
981
+ return JSON.parse(raw);
982
+ } catch {
983
+ return fallback;
984
+ }
985
+ }
986
+ function writeJson(store, key, value) {
987
+ try {
988
+ store.set(key, JSON.stringify(value));
989
+ } catch {
990
+ }
991
+ }
992
+ var KEYS = {
993
+ anon: "guidely:anon",
994
+ seen: "guidely:seen",
995
+ config: (key) => `guidely:config:${key}`,
996
+ etag: (key) => `guidely:etag:${key}`,
997
+ state: (key) => `guidely:state:${key}`,
998
+ user: (key) => `guidely:user:${key}`,
999
+ queue: (key) => `guidely:queue:${key}`,
1000
+ /** Survey answers and guide/checklist progress not yet accepted by the server. */
1001
+ outbox: (key) => `guidely:outbox:${key}`,
1002
+ /** Agent Analytics turns not yet accepted by the server. */
1003
+ agentOutbox: (key) => `guidely:agents:${key}`,
1004
+ session: "guidely:session",
1005
+ active: "guidely:active",
1006
+ authoring: "guidely:authoring",
1007
+ /** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
1008
+ capture: "guidely:capture",
1009
+ capturePending: "guidely:capture:pending",
1010
+ /** Messenger conversations this browser can read: [{ id, token }]. */
1011
+ conversations: (key) => `guidely:conversations:${key}`,
1012
+ /** Announcement ids opened in the messenger's News tab. */
1013
+ newsSeen: (key) => `guidely:news:${key}`
1014
+ };
742
1015
 
743
- // ../sdk-core/dist/chunk-544KSOC3.js
1016
+ // ../sdk-core/dist/chunk-ZAF2KRJA.js
744
1017
  var EventQueue = class {
745
1018
  constructor(opts) {
746
1019
  __publicField2(this, "opts", opts);
@@ -751,17 +1024,34 @@ var EventQueue = class {
751
1024
  __publicField2(this, "inflightIds", /* @__PURE__ */ new Set());
752
1025
  __publicField2(this, "attempt", 0);
753
1026
  __publicField2(this, "destroyed", false);
1027
+ __publicField2(this, "offRecover", null);
754
1028
  __publicField2(this, "flushIntervalMs");
755
1029
  __publicField2(this, "maxQueue");
756
1030
  __publicField2(this, "batchSize");
757
1031
  __publicField2(this, "maxAttempts");
758
1032
  this.flushIntervalMs = opts.flushIntervalMs ?? 2e3;
759
- this.maxQueue = opts.maxQueue ?? 200;
1033
+ this.maxQueue = opts.maxQueue ?? 500;
760
1034
  this.batchSize = opts.batchSize ?? 50;
761
1035
  this.maxAttempts = opts.maxAttempts ?? 5;
762
1036
  const persisted = readJson(opts.store, opts.storageKey, []);
763
1037
  if (Array.isArray(persisted)) this.events = persisted.filter((e) => e && typeof e.eventId === "string").slice(-this.maxQueue);
764
1038
  if (this.events.length) this.schedule();
1039
+ this.offRecover = opts.health?.onRecover(() => this.retryNow()) ?? null;
1040
+ }
1041
+ /** The API is back: drop any pending backoff and send what is queued. */
1042
+ retryNow() {
1043
+ if (this.destroyed) return;
1044
+ if (this.retryTimer) clearTimeout(this.retryTimer);
1045
+ this.retryTimer = null;
1046
+ this.attempt = 0;
1047
+ void this.flush();
1048
+ }
1049
+ retryIn(ms) {
1050
+ if (this.retryTimer || this.destroyed) return;
1051
+ this.retryTimer = setTimeout(() => {
1052
+ this.retryTimer = null;
1053
+ void this.flush();
1054
+ }, ms);
765
1055
  }
766
1056
  get size() {
767
1057
  return this.events.length;
@@ -795,6 +1085,8 @@ var EventQueue = class {
795
1085
  clearTimeout(this.timer);
796
1086
  this.timer = null;
797
1087
  }
1088
+ const wait = this.opts.health?.waitMs() ?? 0;
1089
+ if (wait > 0) return this.retryIn(wait);
798
1090
  this.inflight = true;
799
1091
  const batch = this.events.slice(0, this.batchSize);
800
1092
  this.inflightIds = new Set(batch.map((e) => e.eventId));
@@ -818,26 +1110,20 @@ var EventQueue = class {
818
1110
  if (this.events.length) this.schedule();
819
1111
  return;
820
1112
  }
821
- this.attempt += 1;
822
- if (this.attempt >= this.maxAttempts) {
823
- this.attempt = 0;
824
- return;
825
- }
826
- this.retryTimer = setTimeout(() => {
827
- this.retryTimer = null;
828
- void this.flush();
829
- }, backoffMs(this.attempt - 1));
1113
+ this.attempt = Math.min(this.attempt + 1, this.maxAttempts);
1114
+ const delay = this.attempt >= this.maxAttempts ? 6e4 : backoffMs(this.attempt - 1);
1115
+ this.retryIn(Math.max(delay, this.opts.health?.waitMs() ?? 0));
830
1116
  }
831
1117
  /** Best-effort synchronous flush on unload using `sendBeacon`. */
832
1118
  flushSync() {
833
1119
  if (this.destroyed || !this.opts.enabled() || !this.opts.beacon) return;
834
1120
  const batch = this.events.filter((e) => !this.inflightIds.has(e.eventId)).slice(0, this.batchSize);
835
1121
  if (!batch.length) return;
836
- if (this.opts.beacon(batch)) {
1122
+ if (this.opts.beacon(batch) && (this.opts.health?.reachable ?? true)) {
837
1123
  const ids = new Set(batch.map((e) => e.eventId));
838
1124
  this.events = this.events.filter((e) => !ids.has(e.eventId));
839
- this.persist();
840
1125
  }
1126
+ this.persist();
841
1127
  }
842
1128
  clear() {
843
1129
  this.events = [];
@@ -845,6 +1131,7 @@ var EventQueue = class {
845
1131
  }
846
1132
  destroy() {
847
1133
  this.destroyed = true;
1134
+ this.offRecover?.();
848
1135
  if (this.timer) clearTimeout(this.timer);
849
1136
  if (this.retryTimer) clearTimeout(this.retryTimer);
850
1137
  this.timer = null;
@@ -852,6 +1139,88 @@ var EventQueue = class {
852
1139
  this.persist();
853
1140
  }
854
1141
  };
1142
+ var Outbox = class {
1143
+ constructor(opts) {
1144
+ __publicField2(this, "opts", opts);
1145
+ __publicField2(this, "items");
1146
+ __publicField2(this, "sending", false);
1147
+ __publicField2(this, "attempt", 0);
1148
+ __publicField2(this, "timer", null);
1149
+ __publicField2(this, "destroyed", false);
1150
+ __publicField2(this, "offRecover");
1151
+ __publicField2(this, "maxItems");
1152
+ __publicField2(this, "maxAgeMs");
1153
+ this.maxItems = opts.maxItems ?? 50;
1154
+ this.maxAgeMs = opts.maxAgeMs ?? 7 * 24 * 60 * 60 * 1e3;
1155
+ const stored = readJson(opts.store, opts.storageKey, []);
1156
+ const cutoff = Date.now() - this.maxAgeMs;
1157
+ this.items = Array.isArray(stored) ? stored.filter((i) => i && typeof i.path === "string" && typeof i.key === "string" && i.createdAt > cutoff).slice(-this.maxItems) : [];
1158
+ this.offRecover = opts.health?.onRecover(() => {
1159
+ this.attempt = 0;
1160
+ this.retryIn(0);
1161
+ }) ?? null;
1162
+ if (this.items.length) this.retryIn(0);
1163
+ }
1164
+ get size() {
1165
+ return this.items.length;
1166
+ }
1167
+ /** Queues `POST path` and sends it now (or as soon as the API is reachable). */
1168
+ add(key, path, body) {
1169
+ if (this.destroyed) return;
1170
+ this.items = this.items.filter((i) => i.key !== key);
1171
+ this.items.push({ key, path, body, createdAt: Date.now() });
1172
+ if (this.items.length > this.maxItems) this.items.splice(0, this.items.length - this.maxItems);
1173
+ this.persist();
1174
+ void this.flush();
1175
+ }
1176
+ persist() {
1177
+ writeJson(this.opts.store, this.opts.storageKey, this.items);
1178
+ }
1179
+ retryIn(ms) {
1180
+ if (this.destroyed) return;
1181
+ if (this.timer) clearTimeout(this.timer);
1182
+ this.timer = setTimeout(() => {
1183
+ this.timer = null;
1184
+ void this.flush();
1185
+ }, ms);
1186
+ }
1187
+ /** Sends queued requests in order, one at a time. */
1188
+ async flush() {
1189
+ if (this.destroyed || this.sending || !this.items.length) return;
1190
+ const wait = this.opts.health?.waitMs() ?? 0;
1191
+ if (wait > 0) return this.retryIn(wait);
1192
+ this.sending = true;
1193
+ try {
1194
+ while (this.items.length && !this.destroyed) {
1195
+ const item = this.items[0];
1196
+ let retry = false;
1197
+ try {
1198
+ const res = await this.opts.api.post(item.path, item.body);
1199
+ retry = !res.ok && (res.status === 429 || res.status >= 500);
1200
+ } catch (err) {
1201
+ retry = true;
1202
+ this.opts.onError?.(err);
1203
+ }
1204
+ if (retry) {
1205
+ this.attempt += 1;
1206
+ this.retryIn(Math.max(backoffMs(this.attempt - 1, 1e3, 6e4), this.opts.health?.waitMs() ?? 0));
1207
+ return;
1208
+ }
1209
+ this.attempt = 0;
1210
+ this.items = this.items.filter((i) => i !== item);
1211
+ this.persist();
1212
+ }
1213
+ } finally {
1214
+ this.sending = false;
1215
+ }
1216
+ }
1217
+ destroy() {
1218
+ this.destroyed = true;
1219
+ this.offRecover?.();
1220
+ if (this.timer) clearTimeout(this.timer);
1221
+ this.timer = null;
1222
+ }
1223
+ };
855
1224
  var Emitter = class {
856
1225
  constructor(onError) {
857
1226
  __publicField2(this, "onError", onError);
@@ -920,12 +1289,32 @@ function areMutuallyExclusive(a, b) {
920
1289
  return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
921
1290
  }
922
1291
  function isWithinSchedule(guide, now = Date.now()) {
923
- const s = guide.config.survey;
924
- if (!s) return true;
925
- if (s.startAt && now < Date.parse(s.startAt)) return false;
926
- if (s.endAt && now > Date.parse(s.endAt)) return false;
1292
+ for (const w of [guide.config.schedule, guide.config.survey]) {
1293
+ if (!w) continue;
1294
+ if (w.startAt && now < Date.parse(w.startAt)) return false;
1295
+ if (w.endAt && now > Date.parse(w.endAt)) return false;
1296
+ }
927
1297
  return true;
928
1298
  }
1299
+ var MINUTE = 60 * 1e3;
1300
+ function countsTowardThrottle(guide) {
1301
+ return isExclusiveGuide(guide) && !guide.config.frequency.ignoreThrottling;
1302
+ }
1303
+ function isThrottleEligible(settings, launches, now = Date.now()) {
1304
+ if (!settings || !settings.enabled) return true;
1305
+ const times = launches.map((l) => Date.parse(l)).filter((t) => !Number.isNaN(t));
1306
+ const windowMs = Math.max(1, settings.windowHours) * HOUR;
1307
+ if (times.filter((t) => now - t < windowMs).length >= Math.max(1, settings.maxGuides)) return false;
1308
+ if (settings.minGapMinutes > 0 && times.some((t) => now - t < settings.minGapMinutes * MINUTE)) return false;
1309
+ return true;
1310
+ }
1311
+ function pruneLaunches(launches, settings, now = Date.now()) {
1312
+ const keepMs = Math.max(settings?.windowHours ?? 24, 1) * HOUR + (settings?.minGapMinutes ?? 0) * MINUTE;
1313
+ return launches.filter((l) => {
1314
+ const t = Date.parse(l);
1315
+ return !Number.isNaN(t) && now - t < keepMs;
1316
+ }).slice(-100);
1317
+ }
929
1318
  function dismissedAfterMerge(local, server) {
930
1319
  if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
931
1320
  return local?.dismissedAt ?? server.dismissedAt ?? null;
@@ -934,6 +1323,115 @@ function dismissedAfterMerge(local, server) {
934
1323
  // src/core/client.ts
935
1324
  import { createFlagsClient as createFlagsClient2 } from "@guidelytech/flags";
936
1325
 
1326
+ // src/core/autocapture.ts
1327
+ var TAP_SLOP = 10;
1328
+ var TAP_MAX_MS = 1e3;
1329
+ var HEATMAP_MAX_POINTS = 40;
1330
+ function quoteValue(value) {
1331
+ return `"${value.replace(/["\\]/g, "\\$&")}"`;
1332
+ }
1333
+ function attrSelector(name, value) {
1334
+ return `[${name}=${quoteValue(value)}]`.slice(0, 300);
1335
+ }
1336
+ function cleanText(text) {
1337
+ const t = (text ?? "").replace(/\s+/g, " ").trim();
1338
+ return t ? t.slice(0, 100) : void 0;
1339
+ }
1340
+ function tapProperties(el, x, y, platform, maskAll) {
1341
+ const props = { selector: el.selector.slice(0, 300), tag: el.tag || "View", x: Math.round(x) || 0, y: Math.round(y) || 0, platform };
1342
+ const text = maskAll ? void 0 : cleanText(el.text);
1343
+ if (text) props.text = text;
1344
+ return props;
1345
+ }
1346
+ var r2 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 100) / 100;
1347
+ function heatmapPoint(x, y, el, rect, rage) {
1348
+ const p = { k: rage ? "r" : "c", x: Math.round(x) || 0, y: Math.round(y) || 0 };
1349
+ const s = el?.selector.slice(0, 120);
1350
+ if (s) p.s = s;
1351
+ if (s && rect && rect.width > 0 && rect.height > 0) {
1352
+ p.rx = r2((x - rect.x) / rect.width);
1353
+ p.ry = r2((y - rect.y) / rect.height);
1354
+ }
1355
+ return p;
1356
+ }
1357
+ var RageClickDetector = class {
1358
+ constructor(threshold = 3, windowMs = 1e3, radius = 30) {
1359
+ __publicField(this, "threshold", threshold);
1360
+ __publicField(this, "windowMs", windowMs);
1361
+ __publicField(this, "radius", radius);
1362
+ __publicField(this, "clicks", []);
1363
+ __publicField(this, "fired", false);
1364
+ }
1365
+ /** Taps in the current burst once it has fired (rage), otherwise 0. */
1366
+ get burst() {
1367
+ return this.fired ? this.clicks.length : 0;
1368
+ }
1369
+ /** Records a tap; returns the tap count when it completes a burst, otherwise 0. */
1370
+ add(x, y, t) {
1371
+ const first = this.clicks[0];
1372
+ const last = this.clicks[this.clicks.length - 1];
1373
+ if (!first || !last || t - last.t > this.windowMs || Math.hypot(x - first.x, y - first.y) > this.radius) {
1374
+ this.clicks = [];
1375
+ this.fired = false;
1376
+ }
1377
+ this.clicks = this.clicks.filter((c) => t - c.t <= this.windowMs);
1378
+ this.clicks.push({ x, y, t });
1379
+ if (this.fired || this.clicks.length < this.threshold) return 0;
1380
+ this.fired = true;
1381
+ return this.clicks.length;
1382
+ }
1383
+ };
1384
+ var HeatmapBuffer = class {
1385
+ constructor() {
1386
+ /** Screen path the buffered points belong to. */
1387
+ __publicField(this, "url", "");
1388
+ __publicField(this, "points", []);
1389
+ }
1390
+ /** Adds a tap (`burst` > 0 marks it and the burst's previous taps as rage); true when the batch is full. */
1391
+ add(url, point, burst) {
1392
+ this.url = url;
1393
+ for (let i = Math.max(0, this.points.length - burst + 1); i < this.points.length; i++) this.points[i].k = "r";
1394
+ this.points.push(point);
1395
+ return this.points.length >= HEATMAP_MAX_POINTS;
1396
+ }
1397
+ /** Empties the buffer, returning what was in it. */
1398
+ take() {
1399
+ const points = this.points;
1400
+ this.points = [];
1401
+ return points.length ? { url: this.url, points } : null;
1402
+ }
1403
+ };
1404
+ var TapTracker = class {
1405
+ constructor() {
1406
+ __publicField(this, "start", null);
1407
+ __publicField(this, "moved", 0);
1408
+ }
1409
+ /** A finger went down. A second finger while one is tracked cancels the gesture (not a tap). */
1410
+ begin(x, y, t, touches, id) {
1411
+ if (this.start || touches > 1) {
1412
+ this.start = null;
1413
+ return;
1414
+ }
1415
+ this.start = { x, y, t, id };
1416
+ this.moved = 0;
1417
+ }
1418
+ move(x, y, id) {
1419
+ const s = this.start;
1420
+ 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));
1421
+ }
1422
+ /** The finger lifted: the tap position when it moved ≤ 10pt and lasted < 1s, else null. */
1423
+ end(x, y, t, id) {
1424
+ const s = this.start;
1425
+ if (!s || id !== void 0 && s.id !== void 0 && id !== s.id) return null;
1426
+ this.start = null;
1427
+ const moved = Math.max(this.moved, Math.hypot(x - s.x, y - s.y));
1428
+ return moved <= TAP_SLOP && t - s.t < TAP_MAX_MS ? { x, y } : null;
1429
+ }
1430
+ cancel() {
1431
+ this.start = null;
1432
+ }
1433
+ };
1434
+
937
1435
  // src/core/targets.ts
938
1436
  var TargetTimeoutError = class extends Error {
939
1437
  constructor(ids) {
@@ -1036,6 +1534,35 @@ var TargetRegistry = class {
1036
1534
  return null;
1037
1535
  }
1038
1536
  }
1537
+ /**
1538
+ * The registered target on the current screen whose visible frame contains the point (window
1539
+ * coordinates); the smallest one when targets are nested. Frames that do not answer within
1540
+ * `timeoutMs` are skipped.
1541
+ */
1542
+ async hitTest(x, y, timeoutMs = 500) {
1543
+ const candidates = [];
1544
+ this.targets.forEach((list, id) => {
1545
+ for (const handle of list) if (!(handle.screen && this.screen && handle.screen !== this.screen)) candidates.push({ id, handle });
1546
+ });
1547
+ if (!candidates.length) return null;
1548
+ const rects = await Promise.all(
1549
+ candidates.map(
1550
+ ({ handle }) => new Promise((resolve) => {
1551
+ const timer = setTimeout(() => resolve(null), timeoutMs);
1552
+ void this.measureHandle(handle).then((r) => {
1553
+ clearTimeout(timer);
1554
+ resolve(r);
1555
+ });
1556
+ })
1557
+ )
1558
+ );
1559
+ let best = null;
1560
+ rects.forEach((r, i) => {
1561
+ if (!r || x < r.x || y < r.y || x > r.x + r.width || y > r.y + r.height) return;
1562
+ if (!best || r.width * r.height < best.rect.width * best.rect.height) best = { id: candidates[i].id, rect: r };
1563
+ });
1564
+ return best;
1565
+ }
1039
1566
  /** First of `ids` that is mounted and visible. */
1040
1567
  async resolve(ids) {
1041
1568
  for (const id of ids) {
@@ -1407,12 +1934,29 @@ function createSseParser(onEvent) {
1407
1934
  }
1408
1935
  };
1409
1936
  }
1937
+ var STREAM_RESPONSE_TIMEOUT_MS2 = 3e4;
1938
+ var STREAM_IDLE_TIMEOUT_MS2 = 6e4;
1410
1939
  function xhrStreamPost() {
1411
1940
  if (typeof XMLHttpRequest === "undefined") return null;
1412
1941
  return (url, headers, body, onChunk) => new Promise((resolve, reject) => {
1413
1942
  const xhr = new XMLHttpRequest();
1414
1943
  let seen = 0;
1415
1944
  let streamed = false;
1945
+ let timer = null;
1946
+ let settled = false;
1947
+ const settle = (fn) => {
1948
+ if (settled) return;
1949
+ settled = true;
1950
+ if (timer) clearTimeout(timer);
1951
+ fn();
1952
+ };
1953
+ const arm = (ms, what) => {
1954
+ if (timer) clearTimeout(timer);
1955
+ timer = setTimeout(() => {
1956
+ settle(() => reject(new Error(`Scout ${what} timed out`)));
1957
+ xhr.abort();
1958
+ }, ms);
1959
+ };
1416
1960
  const pump = () => {
1417
1961
  const type = xhr.getResponseHeader("content-type") || "";
1418
1962
  if (!type.includes("text/event-stream")) return;
@@ -1425,8 +1969,15 @@ function xhrStreamPost() {
1425
1969
  };
1426
1970
  xhr.open("POST", url);
1427
1971
  for (const [k, v] of Object.entries(headers)) xhr.setRequestHeader(k, v);
1428
- xhr.onprogress = pump;
1429
- xhr.onload = () => {
1972
+ xhr.onreadystatechange = () => {
1973
+ if (!settled && xhr.readyState >= 2 && xhr.readyState < 4) arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
1974
+ };
1975
+ xhr.onprogress = () => {
1976
+ if (settled) return;
1977
+ arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
1978
+ pump();
1979
+ };
1980
+ xhr.onload = () => settle(() => {
1430
1981
  pump();
1431
1982
  let json = null;
1432
1983
  if (!streamed) {
@@ -1437,27 +1988,74 @@ function xhrStreamPost() {
1437
1988
  }
1438
1989
  }
1439
1990
  resolve({ status: xhr.status, json, streamed });
1440
- };
1441
- xhr.onerror = () => reject(new Error("Network request failed"));
1991
+ });
1992
+ xhr.onerror = () => settle(() => reject(new Error("Network request failed")));
1993
+ arm(STREAM_RESPONSE_TIMEOUT_MS2, "request");
1442
1994
  xhr.send(JSON.stringify(body));
1443
1995
  });
1444
1996
  }
1997
+ function bodyReader(res) {
1998
+ const body = res.body;
1999
+ const Decoder = globalThis.TextDecoder;
2000
+ return body && typeof body.getReader === "function" && Decoder ? { reader: body.getReader(), decoder: new Decoder() } : null;
2001
+ }
1445
2002
  function fetchStreamPost(fetchImpl) {
1446
2003
  return async (url, headers, body, onChunk) => {
1447
- const res = await fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body) });
1448
- const type = res.headers.get("content-type") || "";
1449
- const text = await res.text();
1450
- if (type.includes("text/event-stream")) {
1451
- onChunk(text);
1452
- return { status: res.status, json: null, streamed: true };
1453
- }
1454
- let json = null;
2004
+ const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
2005
+ let timer = null;
2006
+ let timedOut = "";
2007
+ let fail = () => {
2008
+ };
2009
+ const expired = new Promise((_, reject) => fail = reject);
2010
+ expired.catch(() => {
2011
+ });
2012
+ const arm = (ms, what) => {
2013
+ if (timer) clearTimeout(timer);
2014
+ timer = setTimeout(() => {
2015
+ timedOut = what;
2016
+ ac?.abort();
2017
+ fail(new Error(`Scout ${what} timed out`));
2018
+ }, ms);
2019
+ };
2020
+ const race = (p) => Promise.race([p, expired]);
2021
+ arm(STREAM_RESPONSE_TIMEOUT_MS2, "request");
1455
2022
  try {
1456
- json = JSON.parse(text || "null");
1457
- } catch {
1458
- json = null;
2023
+ const res = await race(fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body), signal: ac?.signal }));
2024
+ arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
2025
+ const type = res.headers.get("content-type") || "";
2026
+ const sse = type.includes("text/event-stream");
2027
+ const streaming = sse ? bodyReader(res) : null;
2028
+ if (streaming) {
2029
+ const { reader, decoder } = streaming;
2030
+ for (; ; ) {
2031
+ const { done, value } = await race(reader.read());
2032
+ if (value) {
2033
+ arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
2034
+ onChunk(decoder.decode(value, { stream: true }));
2035
+ }
2036
+ if (done) break;
2037
+ }
2038
+ const rest = decoder.decode();
2039
+ if (rest) onChunk(rest);
2040
+ return { status: res.status, json: null, streamed: true };
2041
+ }
2042
+ const text = await race(res.text());
2043
+ if (sse) {
2044
+ onChunk(text);
2045
+ return { status: res.status, json: null, streamed: true };
2046
+ }
2047
+ let json = null;
2048
+ try {
2049
+ json = JSON.parse(text || "null");
2050
+ } catch {
2051
+ json = null;
2052
+ }
2053
+ return { status: res.status, json, streamed: false };
2054
+ } catch (err) {
2055
+ throw timedOut ? new Error(`Scout ${timedOut} timed out`) : err;
2056
+ } finally {
2057
+ if (timer) clearTimeout(timer);
1459
2058
  }
1460
- return { status: res.status, json, streamed: false };
1461
2059
  };
1462
2060
  }
1463
2061
  var byNewest = (a, b) => Date.parse(b.lastMessageAt) - Date.parse(a.lastMessageAt);
@@ -1642,7 +2240,15 @@ var Messenger = class {
1642
2240
  }
1643
2241
  });
1644
2242
  try {
1645
- const res = await this.deps.streamPost(this.url(path), headers, payload, (chunk) => parser.feed(chunk));
2243
+ let res;
2244
+ try {
2245
+ res = await this.deps.streamPost(this.url(path), headers, payload, (chunk) => parser.feed(chunk));
2246
+ } catch (err) {
2247
+ this.deps.health?.failure();
2248
+ throw err;
2249
+ }
2250
+ if (isServerFailure(res.status)) this.deps.health?.failure();
2251
+ else this.deps.health?.success();
1646
2252
  parser.end();
1647
2253
  if (res.status < 200 || res.status >= 300) throw new Error(`Request failed (${res.status})`);
1648
2254
  if (!res.streamed && res.json) {
@@ -1714,7 +2320,8 @@ var Messenger = class {
1714
2320
  this.stopThreadPolling();
1715
2321
  this.set({ open: true, view: { kind: "article", articleId: id, from }, article: null });
1716
2322
  try {
1717
- const res = await this.request("GET", `/articles/${encodeURIComponent(id)}`);
2323
+ const locale = this.deps.locale?.();
2324
+ const res = await this.request("GET", `/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : {});
1718
2325
  this.set({ article: res.article });
1719
2326
  } catch (err) {
1720
2327
  this.deps.onError(err);
@@ -1829,9 +2436,22 @@ var PersistentStore = class {
1829
2436
 
1830
2437
  // src/core/client.ts
1831
2438
  var SDK_NAME = "@guidelytech/react-native";
1832
- var SDK_VERSION = "0.9.0";
2439
+ var SDK_VERSION = "0.11.0";
1833
2440
  var DEFAULT_API_URL = "https://guidelytech.com";
1834
2441
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
2442
+ var CACHED_START_MS = 1500;
2443
+ function settlesWithin(promise, ms) {
2444
+ return new Promise((resolve) => {
2445
+ const timer = setTimeout(() => resolve(false), ms);
2446
+ const done = () => {
2447
+ clearTimeout(timer);
2448
+ resolve(true);
2449
+ };
2450
+ promise.then(done, done);
2451
+ });
2452
+ }
2453
+ var appVersionKey = (key) => `guidely:app:${key}`;
2454
+ var UNKNOWN_VIEW = { selector: "View", tag: "View" };
1835
2455
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
1836
2456
  var emptyState = () => ({ guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] });
1837
2457
  var newSession = () => ({ id: `s_${randomId(16)}`, startedAt: nowIso(), lastActiveAt: nowIso(), shownGuides: [], events: [] });
@@ -1844,6 +2464,8 @@ var GuidelyClient = class {
1844
2464
  __publicField(this, "store");
1845
2465
  __publicField(this, "emitter");
1846
2466
  __publicField(this, "queue", null);
2467
+ /** Survey answers, guide completions/dismissals and checklist progress, kept until the server has them. */
2468
+ __publicField(this, "outbox", null);
1847
2469
  __publicField(this, "config", null);
1848
2470
  __publicField(this, "configFetchedAt", 0);
1849
2471
  __publicField(this, "etag", null);
@@ -1861,6 +2483,7 @@ var GuidelyClient = class {
1861
2483
  __publicField(this, "activeSurvey", null);
1862
2484
  __publicField(this, "queued", []);
1863
2485
  __publicField(this, "flows", /* @__PURE__ */ new Map());
2486
+ /** Checklists on screen, with the guide as rendered (in the visitor's language). */
1864
2487
  __publicField(this, "checklists", /* @__PURE__ */ new Map());
1865
2488
  __publicField(this, "expanded", /* @__PURE__ */ new Set());
1866
2489
  __publicField(this, "surveyView", null);
@@ -1880,6 +2503,27 @@ var GuidelyClient = class {
1880
2503
  __publicField(this, "flags");
1881
2504
  /** Experiments already reported this session (`experiment_viewed` is sent once per session). */
1882
2505
  __publicField(this, "exposures", /* @__PURE__ */ new Set());
2506
+ // Product analytics (autocapture).
2507
+ __publicField(this, "rage", new RageClickDetector());
2508
+ __publicField(this, "heatmap", new HeatmapBuffer());
2509
+ /** Taps are processed in order (target frames are measured asynchronously). */
2510
+ __publicField(this, "tapChain", Promise.resolve());
2511
+ /** When the current screen's foreground measurement started; `screenLeft` once it has been reported. */
2512
+ __publicField(this, "screenStartedAt", 0);
2513
+ __publicField(this, "screenLeft", true);
2514
+ /** Last `app_open` (cold start or return to the foreground), for `app_background.foregroundMs`. */
2515
+ __publicField(this, "openedAt", Date.now());
2516
+ __publicField(this, "launchChecked", false);
2517
+ /** An identity was stored before this launch (an existing install adopting lifecycle events). */
2518
+ __publicField(this, "hadIdentity", false);
2519
+ /** Language set with `setLocale()` (or the `locale` option). */
2520
+ __publicField(this, "locale", null);
2521
+ /** Language each shown guide was rendered in; added to its events as `locale`. */
2522
+ __publicField(this, "guideLocales", /* @__PURE__ */ new Map());
2523
+ /** The resource center in the visitor's language, recomputed when the config or language changes. */
2524
+ __publicField(this, "localizedRc", null);
2525
+ /** "What's new" last-seen time when the resource center opened (entries after it read as new). */
2526
+ __publicField(this, "announcementsSeenAtOpen", null);
1883
2527
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
1884
2528
  __publicField(this, "eventBus", {
1885
2529
  on: (name, cb) => {
@@ -1906,9 +2550,10 @@ var GuidelyClient = class {
1906
2550
  this.emitter = new Emitter((err) => this.reportError(err));
1907
2551
  this.api = new Api(this.options.apiUrl, this.options.projectId, options.fetch);
1908
2552
  this.store = new PersistentStore(options.storage ?? null, (err) => this.log("storage error", err));
1909
- if (!options.storage) this.log("no `storage` passed: identity and guide history last only for this app session. Pass AsyncStorage to persist them.");
2553
+ 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.");
1910
2554
  this.cleanups.push(this.targets.onTap((id) => this.handleTap(id)));
1911
2555
  if (options.user) this.user = { ...options.user };
2556
+ this.locale = options.locale ? normalizeLocale(options.locale) || null : null;
1912
2557
  this.flags = this.createFlags(options.flags);
1913
2558
  }
1914
2559
  createFlags(opts) {
@@ -1957,9 +2602,10 @@ var GuidelyClient = class {
1957
2602
  async bootstrap() {
1958
2603
  const key = this.options.projectId;
1959
2604
  try {
1960
- 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)]);
2605
+ 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)]);
1961
2606
  if (this.destroyed) return;
1962
2607
  this.anonymousId = this.store.get(KEYS.anon) || "";
2608
+ this.hadIdentity = !!this.anonymousId;
1963
2609
  if (!this.anonymousId) {
1964
2610
  this.anonymousId = `anon_${randomId(20)}`;
1965
2611
  this.store.set(KEYS.anon, this.anonymousId);
@@ -1977,22 +2623,28 @@ var GuidelyClient = class {
1977
2623
  store: this.store,
1978
2624
  storageKey: KEYS.queue(key),
1979
2625
  enabled: () => this.analyticsEnabled(),
2626
+ health: this.api.health,
1980
2627
  send: async (events) => {
1981
2628
  const res = await this.api.post("/events", { events, sentAt: nowIso(), sdk: { name: SDK_NAME, version: SDK_VERSION } });
1982
2629
  return { ok: res.ok, retry: !(res.status >= 400 && res.status < 500 && res.status !== 429) };
1983
2630
  },
1984
2631
  onError: (err) => this.reportError(err)
1985
2632
  });
2633
+ this.outbox = new Outbox({ api: this.api, store: this.store, storageKey: KEYS.outbox(key), health: this.api.health });
1986
2634
  this.hydrated = true;
1987
2635
  const cached = readJson(this.store, KEYS.config(key), null);
1988
2636
  this.etag = this.store.get(KEYS.etag(key));
1989
2637
  if (cached && Array.isArray(cached.guides)) this.config = forPlatform(cached, this.bridge.device.platform);
1990
2638
  this.syncMessenger();
1991
2639
  this.pending.splice(0).forEach((fn) => fn());
1992
- await this.fetchConfig();
2640
+ const before = this.config?.configVersion;
2641
+ const fresh = this.fetchConfig();
2642
+ if (!this.config) await fresh;
2643
+ else if (!await settlesWithin(fresh, CACHED_START_MS)) void fresh.then(() => this.applyConfigChange(before));
1993
2644
  if (this.destroyed) return;
2645
+ this.checkLaunch();
1994
2646
  this.wireTimers();
1995
- if (this.user?.id) await this.identify(this.user.id, this.user.traits, this.user.account);
2647
+ if (this.user?.id) await settlesWithin(this.identify(this.user.id, this.user.traits, this.user.account), CACHED_START_MS);
1996
2648
  if (this.destroyed) return;
1997
2649
  this.isReady = true;
1998
2650
  if (this.config) this.emitter.emit("ready", this.config);
@@ -2020,6 +2672,7 @@ var GuidelyClient = class {
2020
2672
  if (res.etag) this.store.set(KEYS.etag(this.options.projectId), res.etag);
2021
2673
  else this.store.remove(KEYS.etag(this.options.projectId));
2022
2674
  this.syncMessenger();
2675
+ if (this.hydrated) this.checkLaunch();
2023
2676
  this.emitter.emit("config", this.config);
2024
2677
  this.log("config loaded", this.config.configVersion, `${this.config.guides.length} guides`);
2025
2678
  return true;
@@ -2032,13 +2685,15 @@ var GuidelyClient = class {
2032
2685
  }
2033
2686
  }
2034
2687
  wireTimers() {
2035
- const refresh = setInterval(() => void this.refresh(), this.options.configRefreshMs);
2688
+ const refresh = setInterval(() => {
2689
+ if (this.api.health.canSend()) void this.refresh();
2690
+ }, this.options.configRefreshMs);
2036
2691
  this.cleanups.push(() => clearInterval(refresh));
2037
2692
  const poll = setInterval(() => {
2038
2693
  if (!this.isReady || !this.config || this.backgroundedAt !== null) return;
2039
2694
  const url = this.currentUrl();
2040
2695
  const ctx = this.getContext();
2041
- for (const guide of this.config.guides) if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
2696
+ for (const guide of this.candidates()) if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
2042
2697
  this.drainQueue(url, ctx);
2043
2698
  }, 1e3);
2044
2699
  this.cleanups.push(() => clearInterval(poll));
@@ -2048,6 +2703,11 @@ var GuidelyClient = class {
2048
2703
  if (this.destroyed) return;
2049
2704
  const before = this.config?.configVersion;
2050
2705
  await this.fetchConfig();
2706
+ this.applyConfigChange(before);
2707
+ }
2708
+ /** Re-renders after a config change (refresh, or a config that arrived after start). */
2709
+ applyConfigChange(before) {
2710
+ if (this.destroyed || !this.isReady) return;
2051
2711
  if (this.config && this.config.configVersion !== before) {
2052
2712
  this.notify();
2053
2713
  this.evaluate("ready");
@@ -2057,10 +2717,13 @@ var GuidelyClient = class {
2057
2717
  setAppState(state) {
2058
2718
  if (this.destroyed || !this.hydrated) return;
2059
2719
  if (state === "background") {
2720
+ const wasForeground = this.backgroundedAt === null;
2060
2721
  this.backgroundedAt = Date.now();
2722
+ if (wasForeground) this.wentToBackground();
2061
2723
  this.session.lastActiveAt = nowIso();
2062
2724
  this.persistSession();
2063
2725
  void this.queue?.flush();
2726
+ void this.outbox?.flush();
2064
2727
  void this.store.flush();
2065
2728
  void this.flags.flush();
2066
2729
  this.messenger?.setForeground(false);
@@ -2073,8 +2736,9 @@ var GuidelyClient = class {
2073
2736
  this.persistSession();
2074
2737
  this.firedDelays.clear();
2075
2738
  }
2739
+ this.cameToForeground();
2076
2740
  void this.flags.refresh();
2077
- if (Date.now() - this.configFetchedAt > this.options.configRefreshMs) void this.refresh();
2741
+ if (Date.now() - this.configFetchedAt > this.options.configRefreshMs && this.api.health.canSend()) void this.refresh();
2078
2742
  else if (this.isReady) this.evaluate("ready");
2079
2743
  }
2080
2744
  }
@@ -2085,6 +2749,7 @@ var GuidelyClient = class {
2085
2749
  /** Tears everything down. Create a new client to start again. */
2086
2750
  destroy() {
2087
2751
  if (this.destroyed) return;
2752
+ this.flushHeatmap();
2088
2753
  this.destroyed = true;
2089
2754
  this.active?.controller.destroy();
2090
2755
  this.active = null;
@@ -2100,6 +2765,7 @@ var GuidelyClient = class {
2100
2765
  this.cleanups.splice(0).forEach((fn) => fn());
2101
2766
  void this.queue?.flush();
2102
2767
  this.queue?.destroy();
2768
+ this.outbox?.destroy();
2103
2769
  this.flags.destroy();
2104
2770
  this.emitter.clear();
2105
2771
  this.uiListeners.clear();
@@ -2135,6 +2801,7 @@ var GuidelyClient = class {
2135
2801
  for (const [id, s] of Object.entries(data.guideState || {})) {
2136
2802
  const local = guides[id];
2137
2803
  guides[id] = {
2804
+ ...local,
2138
2805
  impressions: Math.max(local?.impressions ?? 0, s.impressions ?? 0),
2139
2806
  completedAt: local?.completedAt ?? s.completedAt ?? null,
2140
2807
  dismissedAt: local?.dismissedAt ?? s.dismissedAt ?? null,
@@ -2156,7 +2823,10 @@ var GuidelyClient = class {
2156
2823
  checklists,
2157
2824
  surveysSubmitted: Array.from(/* @__PURE__ */ new Set([...this.state.surveysSubmitted, ...data.surveysSubmitted || []])),
2158
2825
  audienceIds: data.audienceIds || [],
2159
- firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt
2826
+ firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt,
2827
+ // Device-local only (the server doesn't hold them).
2828
+ ...this.state.throttle ? { throttle: this.state.throttle } : {},
2829
+ ...this.state.announcementsSeenAt ? { announcementsSeenAt: this.state.announcementsSeenAt } : {}
2160
2830
  };
2161
2831
  if (data.firstSeenAt && Date.now() - Date.parse(data.firstSeenAt) > 6e4) this.firstVisit = false;
2162
2832
  this.persistState();
@@ -2248,6 +2918,8 @@ var GuidelyClient = class {
2248
2918
  ...extra
2249
2919
  };
2250
2920
  if (extra.properties) event.properties = sanitizeProperties(extra.properties);
2921
+ const locale = extra.guideId ? this.guideLocales.get(extra.guideId) : void 0;
2922
+ if (locale) event.properties = { ...event.properties ?? {}, locale };
2251
2923
  this.queue.enqueue(event);
2252
2924
  this.emitter.emit("track", event);
2253
2925
  return event;
@@ -2280,11 +2952,19 @@ var GuidelyClient = class {
2280
2952
  if (!name) return;
2281
2953
  this.whenHydrated(() => {
2282
2954
  const changed = name !== this.screenName;
2955
+ if (changed) {
2956
+ this.leaveScreen();
2957
+ this.flushHeatmap();
2958
+ }
2283
2959
  this.screenName = name;
2284
2960
  this.targets.setScreen(name);
2285
2961
  if (changed) this.syncFlagAttributes();
2286
2962
  const url = this.currentUrl();
2287
- if (changed) this.enqueue("page_view", { properties: { ...properties, path: url, screen: name } });
2963
+ if (changed) {
2964
+ this.screenStartedAt = Date.now();
2965
+ this.screenLeft = this.backgroundedAt !== null;
2966
+ if (this.pageviewsOn()) this.enqueue("page_view", { properties: { ...properties, path: url, screen: name, platform: this.bridge.device.platform } });
2967
+ }
2288
2968
  this.completeChecklistTasksBy("url_visited", url);
2289
2969
  this.active?.controller.resume();
2290
2970
  this.flows.forEach((f) => f.controller.resume());
@@ -2312,6 +2992,149 @@ var GuidelyClient = class {
2312
2992
  if (this.isReady) this.evaluate("click", { targetId });
2313
2993
  }
2314
2994
  // ---------------------------------------------------------------------------
2995
+ // Product analytics (autocapture)
2996
+ // ---------------------------------------------------------------------------
2997
+ captureAllowed() {
2998
+ return !this.destroyed && this.analyticsEnabled();
2999
+ }
3000
+ /** A capability runs when the project enables it (absent on old servers → off), no option turns it off and analytics are allowed. */
3001
+ autocaptureOn(key) {
3002
+ const o = this.options.autocapture;
3003
+ return !!this.config?.settings?.autocapture?.[key] && o !== false && !(typeof o === "object" && o?.[key] === false) && this.captureAllowed();
3004
+ }
3005
+ /** `page_view` from `screen()` predates autocapture: only an explicit opt-out stops it. */
3006
+ pageviewsOn() {
3007
+ const o = this.options.autocapture;
3008
+ return this.config?.settings?.autocapture?.pageviews !== false && o !== false && !(typeof o === "object" && o?.pageviews === false);
3009
+ }
3010
+ /** True when taps are worth observing (clicks, rage taps or heatmaps are on); the provider skips resolving views otherwise. */
3011
+ isTapCaptureOn() {
3012
+ return this.hydrated && (this.autocaptureOn("clicks") || this.autocaptureOn("rageClicks") || this.autocaptureOn("heatmaps"));
3013
+ }
3014
+ /**
3015
+ * Records a tap observed by the provider: `click` for an interactive element, `rage_click` for
3016
+ * bursts and a heatmap point. A registered `<GuidelyTarget>` whose frame contains the point
3017
+ * (smallest wins) takes precedence over the resolved element and is reported as `#<id>`.
3018
+ */
3019
+ recordTap(tap) {
3020
+ try {
3021
+ if (!this.hydrated || this.destroyed || !Number.isFinite(tap.x) || !Number.isFinite(tap.y)) return;
3022
+ const on = { clicks: this.autocaptureOn("clicks"), rage: this.autocaptureOn("rageClicks"), heat: this.autocaptureOn("heatmaps") };
3023
+ if (!on.clicks && !on.rage && !on.heat) return;
3024
+ const t = tap.t ?? Date.now();
3025
+ const url = this.screenName ? this.currentUrl() : "";
3026
+ this.tapChain = this.tapChain.then(() => this.processTap(tap, t, url, on)).catch((err) => this.reportError(err));
3027
+ } catch (err) {
3028
+ this.reportError(err);
3029
+ }
3030
+ }
3031
+ async processTap(tap, t, url, on) {
3032
+ let el = tap.element ?? null;
3033
+ let rect = null;
3034
+ const target = this.targets.ids().length ? await this.targets.hitTest(tap.x, tap.y) : null;
3035
+ if (target) {
3036
+ el = { selector: `#${target.id}`, tag: el?.tag ?? tap.hit?.tag ?? "View", text: el?.text };
3037
+ rect = target.rect;
3038
+ }
3039
+ if (this.destroyed) return;
3040
+ const platform = this.bridge.device.platform;
3041
+ const maskAll = !!this.config?.settings?.replay?.maskAllText;
3042
+ const eventUrl = this.collectUrls() && url ? url : void 0;
3043
+ if (on.clicks && el) this.enqueue("click", { url: eventUrl, properties: { ...tapProperties(el, tap.x, tap.y, platform, maskAll) } });
3044
+ const count = this.rage.add(tap.x, tap.y, t);
3045
+ if (on.rage && count) this.enqueue("rage_click", { url: eventUrl, properties: { ...tapProperties(el ?? tap.hit ?? UNKNOWN_VIEW, tap.x, tap.y, platform, maskAll), count } });
3046
+ if (!on.heat) return;
3047
+ const burst = this.rage.burst;
3048
+ if (el && !rect && el.measure) rect = await el.measure().catch(() => null);
3049
+ if (this.destroyed) return;
3050
+ if (this.heatmap.url !== url) this.flushHeatmap();
3051
+ if (this.heatmap.add(url, heatmapPoint(tap.x, tap.y, el, rect, burst > 0), burst)) this.flushHeatmap();
3052
+ }
3053
+ /** Sends the buffered heatmap taps as one `heatmap` event attributed to the screen they happened on. */
3054
+ flushHeatmap() {
3055
+ const batch = this.heatmap.take();
3056
+ if (!batch || !this.captureAllowed()) return;
3057
+ const collect = this.collectUrls() && !!batch.url;
3058
+ const { width, height } = this.windowSize();
3059
+ this.enqueue("heatmap", {
3060
+ url: collect ? batch.url : void 0,
3061
+ properties: { ...collect ? { path: batch.url } : {}, viewportWidth: width, viewportHeight: height, pageHeight: height, platform: this.bridge.device.platform, points: batch.points }
3062
+ });
3063
+ }
3064
+ windowSize() {
3065
+ try {
3066
+ const s = this.bridge.windowSize?.();
3067
+ if (s && Number.isFinite(s.width) && Number.isFinite(s.height)) return { width: Math.round(s.width), height: Math.round(s.height) };
3068
+ } catch {
3069
+ }
3070
+ return { width: 0, height: 0 };
3071
+ }
3072
+ /** Ends the foreground measurement of the current screen with a `page_leave` (once per measurement). */
3073
+ leaveScreen() {
3074
+ if (!this.screenName || this.screenLeft) return;
3075
+ this.screenLeft = true;
3076
+ if (!this.autocaptureOn("pageLeave")) return;
3077
+ const url = this.currentUrl();
3078
+ const collect = this.collectUrls();
3079
+ this.enqueue("page_leave", {
3080
+ url: collect ? url : void 0,
3081
+ properties: { ...collect ? { path: url } : {}, screen: this.screenName, durationMs: Math.max(0, Date.now() - this.screenStartedAt), scrollDepth: 100, platform: this.bridge.device.platform }
3082
+ });
3083
+ }
3084
+ /** `{ appVersion, build }` of this launch, without unknown keys. */
3085
+ appInfo() {
3086
+ const appVersion = this.options.appVersion ?? this.bridge.device.appVersion;
3087
+ const build = this.options.appBuild;
3088
+ return { ...appVersion ? { appVersion: String(appVersion) } : {}, ...build ? { build: String(build) } : {} };
3089
+ }
3090
+ /**
3091
+ * Once per launch, as soon as a config is known: `app_install` / `app_update` from the stored
3092
+ * version (when the app passes `appVersion` and `storage`), then `app_open` with `coldStart: true`.
3093
+ */
3094
+ checkLaunch() {
3095
+ if (this.launchChecked || !this.config || this.destroyed || !this.hydrated) return;
3096
+ this.launchChecked = true;
3097
+ try {
3098
+ const on = this.autocaptureOn("appLifecycle");
3099
+ const info = this.appInfo();
3100
+ if (info.appVersion && this.options.storage) {
3101
+ const key = appVersionKey(this.options.projectId);
3102
+ const stored = readJson(this.store, key, null);
3103
+ if (!stored?.appVersion) {
3104
+ if (!this.hadIdentity && on) this.enqueue("app_install", { properties: { ...info } });
3105
+ } else if (stored.appVersion !== info.appVersion || !!stored.build && !!info.build && stored.build !== info.build) {
3106
+ if (on) this.enqueue("app_update", { properties: { ...info, previousVersion: stored.appVersion, ...stored.build ? { previousBuild: stored.build } : {} } });
3107
+ }
3108
+ writeJson(this.store, key, info);
3109
+ }
3110
+ if (on) this.enqueue("app_open", { properties: { ...info, coldStart: true } });
3111
+ } catch (err) {
3112
+ this.reportError(err);
3113
+ }
3114
+ }
3115
+ wentToBackground() {
3116
+ try {
3117
+ this.leaveScreen();
3118
+ this.flushHeatmap();
3119
+ if (this.launchChecked && this.autocaptureOn("appLifecycle")) this.enqueue("app_background", { properties: { foregroundMs: Math.max(0, Date.now() - this.openedAt) } });
3120
+ } catch (err) {
3121
+ this.reportError(err);
3122
+ }
3123
+ }
3124
+ cameToForeground() {
3125
+ try {
3126
+ this.openedAt = Date.now();
3127
+ if (this.launchChecked && this.autocaptureOn("appLifecycle")) this.enqueue("app_open", { properties: { ...this.appInfo(), coldStart: false } });
3128
+ if (this.screenName && this.screenLeft) {
3129
+ this.screenStartedAt = Date.now();
3130
+ this.screenLeft = false;
3131
+ if (this.pageviewsOn() && this.autocaptureOn("pageLeave")) this.enqueue("page_view", { properties: { path: this.currentUrl(), screen: this.screenName, platform: this.bridge.device.platform, resumed: true } });
3132
+ }
3133
+ } catch (err) {
3134
+ this.reportError(err);
3135
+ }
3136
+ }
3137
+ // ---------------------------------------------------------------------------
2315
3138
  // Targeting + triggers
2316
3139
  // ---------------------------------------------------------------------------
2317
3140
  /** Builds the targeting context for the current moment. */
@@ -2358,6 +3181,50 @@ var GuidelyClient = class {
2358
3181
  if (guide.kind === "checklist") return !this.state.checklists[guide.id]?.dismissedAt;
2359
3182
  return isFrequencyEligible(guide.config.frequency, this.state.guides[guide.id], this.session.shownGuides.includes(guide.id));
2360
3183
  }
3184
+ /** Guide throttling: may this guide launch automatically now? */
3185
+ throttleAllows(guide) {
3186
+ if (!countsTowardThrottle(guide)) return true;
3187
+ return isThrottleEligible(this.config?.settings.throttling, this.state.throttle ?? []);
3188
+ }
3189
+ recordLaunch(guide, manual) {
3190
+ if (manual || !countsTowardThrottle(guide) || !this.config?.settings.throttling?.enabled) return;
3191
+ this.state.throttle = pruneLaunches([...this.state.throttle ?? [], nowIso()], this.config.settings.throttling);
3192
+ }
3193
+ /** Guides and A/B test control groups, which are evaluated the same way but never render. */
3194
+ candidates() {
3195
+ const guides = this.config?.guides ?? [];
3196
+ const holdouts = this.config?.guideHoldouts ?? [];
3197
+ return holdouts.length ? [...guides, ...holdouts.map((h) => h.guide)] : guides;
3198
+ }
3199
+ holdoutFor(guide) {
3200
+ return this.config?.guideHoldouts?.find((h) => h.guide.id === guide.id) ?? null;
3201
+ }
3202
+ // ---------------------------------------------------------------------------
3203
+ // Localization
3204
+ // ---------------------------------------------------------------------------
3205
+ /** The visitor's language candidates, most preferred first: `setLocale()` / `locale` option, traits, device. */
3206
+ locales() {
3207
+ return visitorLocales(this.locale, this.user?.traits, [this.bridge.device.locale]);
3208
+ }
3209
+ defaultLocale() {
3210
+ return this.config?.settings?.localization?.defaultLocale || "en";
3211
+ }
3212
+ /** The guide in the visitor's language (remembered so its events carry `locale`). */
3213
+ localize(guide) {
3214
+ const localized = localizeGuide(guide, this.locales(), this.defaultLocale());
3215
+ this.guideLocales.set(guide.id, localized.locale);
3216
+ return localized;
3217
+ }
3218
+ /**
3219
+ * Sets the language for guides and the resource center (`null` = back to the user's traits and
3220
+ * the device language). Guides already on screen keep their language; the resource center re-renders.
3221
+ */
3222
+ setLocale(locale) {
3223
+ const next = locale ? normalizeLocale(locale) || null : null;
3224
+ if (next === this.locale) return;
3225
+ this.locale = next;
3226
+ this.notify();
3227
+ }
2361
3228
  triggerFires(guide, reason, detail) {
2362
3229
  const t = guide.config.trigger;
2363
3230
  switch (t.type) {
@@ -2393,7 +3260,7 @@ var GuidelyClient = class {
2393
3260
  this.lastAudiences = current;
2394
3261
  }
2395
3262
  const candidates = [];
2396
- for (const guide of this.config.guides) {
3263
+ for (const guide of this.candidates()) {
2397
3264
  const t = guide.config.trigger;
2398
3265
  if (detail.endedGuide && detail.endedGuide.id === guide.id) continue;
2399
3266
  if (reason === "ready" || reason === "navigation") {
@@ -2409,6 +3276,10 @@ var GuidelyClient = class {
2409
3276
  if (t.type === "page_load" && t.delayMs > 0 && reason === "timer" && detail.guideId !== guide.id) continue;
2410
3277
  if (!this.triggerFires(guide, reason, detail)) continue;
2411
3278
  if (!this.isEligible(guide, ctx, url)) continue;
3279
+ if (!this.throttleAllows(guide)) {
3280
+ this.log("throttled", guide.key);
3281
+ continue;
3282
+ }
2412
3283
  candidates.push(guide);
2413
3284
  }
2414
3285
  if (reason === "after_guide" || reason === "navigation") this.drainQueue(url, ctx);
@@ -2454,6 +3325,12 @@ var GuidelyClient = class {
2454
3325
  }
2455
3326
  tryShow(guide, url, manual) {
2456
3327
  if (this.destroyed) return false;
3328
+ if (!manual && !this.throttleAllows(guide)) return false;
3329
+ if (this.holdoutFor(guide)) {
3330
+ if (this.hasExclusive() && !manual) return false;
3331
+ this.recordImpression(guide, manual);
3332
+ return true;
3333
+ }
2457
3334
  if (!isExclusiveGuide(guide)) {
2458
3335
  if (this.flows.has(guide.id) || this.checklists.has(guide.id)) return false;
2459
3336
  this.show(guide, manual);
@@ -2495,14 +3372,18 @@ var GuidelyClient = class {
2495
3372
  guideState(id) {
2496
3373
  return this.state.guides[id] ?? (this.state.guides[id] = { impressions: 0 });
2497
3374
  }
2498
- recordImpression(guide) {
3375
+ recordImpression(guide, manual = false) {
2499
3376
  const s = this.guideState(guide.id);
2500
3377
  s.impressions += 1;
2501
3378
  s.lastShownAt = nowIso();
2502
3379
  if (!this.session.shownGuides.includes(guide.id)) this.session.shownGuides.push(guide.id);
3380
+ this.recordLaunch(guide, manual);
2503
3381
  this.persistState();
2504
3382
  this.persistSession();
2505
- this.syncGuideState(guide.id, { impression: true });
3383
+ const holdout = this.holdoutFor(guide);
3384
+ const experiment = holdout ? { id: holdout.experimentId, variantId: holdout.variantId } : guide.experiment;
3385
+ if (experiment) this.enqueue("experiment_viewed", { properties: { guideExperimentId: experiment.id, variantId: experiment.variantId, ...holdout ? {} : { guideId: guide.id } } });
3386
+ if (!holdout) this.syncGuideState(guide.id, { impression: true });
2506
3387
  }
2507
3388
  recordEnd(guide, reason) {
2508
3389
  const s = this.guideState(guide.id);
@@ -2513,7 +3394,13 @@ var GuidelyClient = class {
2513
3394
  if (reason === "completed") this.completeChecklistTasksBy("guide_completed", guide);
2514
3395
  }
2515
3396
  syncGuideState(guideId, update) {
2516
- void postWithRetry(this.api, "/guide-state", { anonymousId: this.anonymousId, userId: this.user?.id || null, updates: [{ guideId, ...update }] });
3397
+ const body = { anonymousId: this.anonymousId, userId: this.user?.id || null, updates: [{ guideId, ...update }] };
3398
+ if (update.impression) void postWithRetry(this.api, "/guide-state", body);
3399
+ else this.sendDurably(`guide:${guideId}:${update.completed ? "completed" : "dismissed"}:${this.anonymousId}`, "/guide-state", body);
3400
+ }
3401
+ /** Queues a write in the persisted outbox (sent now, or once the API is reachable again). */
3402
+ sendDurably(key, path, body) {
3403
+ this.whenHydrated(() => this.outbox?.add(key, path, body));
2517
3404
  }
2518
3405
  show(guide, manual, stepId) {
2519
3406
  try {
@@ -2524,7 +3411,8 @@ var GuidelyClient = class {
2524
3411
  this.reportError(err);
2525
3412
  }
2526
3413
  }
2527
- showFlow(guide, manual, stepId) {
3414
+ showFlow(raw, manual, stepId) {
3415
+ const guide = this.localize(raw);
2528
3416
  const exclusive = isExclusiveGuide(guide);
2529
3417
  const flow = { guide, manual, view: null, controller: null };
2530
3418
  let renders = 0;
@@ -2539,7 +3427,7 @@ var GuidelyClient = class {
2539
3427
  const step = e.stepId ? guide.config.steps.find((s) => s.id === e.stepId) : void 0;
2540
3428
  this.enqueue(e.name, { guideId: guide.id, guideVersion: guide.version, stepId: e.stepId, stepIndex: e.stepIndex, properties: e.properties });
2541
3429
  if (e.name === "guide_started") {
2542
- this.recordImpression(guide);
3430
+ this.recordImpression(guide, manual);
2543
3431
  this.emitter.emit("guide:start", guide);
2544
3432
  } else if (e.name === "step_viewed" && step) {
2545
3433
  this.emitter.emit("guide:step", { guide, stepId: step.id, stepIndex: e.stepIndex ?? 0 });
@@ -2586,21 +3474,24 @@ var GuidelyClient = class {
2586
3474
  if (this.flows.get(guide.id) === flow) this.flows.delete(guide.id);
2587
3475
  }
2588
3476
  }
2589
- showChecklist(guide, open) {
3477
+ showChecklist(raw, open) {
3478
+ if (!raw.config.checklist || this.checklists.has(raw.id)) return;
3479
+ const guide = this.localize(raw);
2590
3480
  const config = guide.config.checklist;
2591
- if (!config || this.checklists.has(guide.id)) return;
3481
+ if (!config) return;
2592
3482
  const progress = this.getChecklistProgress(guide.id);
2593
3483
  const first = !this.state.guides[guide.id]?.impressions;
2594
- this.checklists.set(guide.id, { open: open ?? (config.autoOpen && first && !progress.completedAt) });
3484
+ this.checklists.set(guide.id, { guide, open: open ?? (config.autoOpen && first && !progress.completedAt) });
2595
3485
  if (first) this.recordImpression(guide);
2596
3486
  if (this.checklists.get(guide.id)?.open) this.enqueue("guide_viewed", { guideId: guide.id, guideVersion: guide.version });
2597
3487
  this.notify();
2598
3488
  }
2599
- showSurvey(guide, manual) {
2600
- if (!guide.config.survey) return;
3489
+ showSurvey(raw, manual) {
3490
+ if (!raw.config.survey) return;
3491
+ const guide = this.localize(raw);
2601
3492
  this.activeSurvey = { guide, manual };
2602
3493
  this.surveyView = { key: `${guide.id}:${randomId(6)}`, guide, submitted: false };
2603
- this.recordImpression(guide);
3494
+ this.recordImpression(guide, manual);
2604
3495
  this.enqueue("survey_displayed", { guideId: guide.id, guideVersion: guide.version });
2605
3496
  this.emitter.emit("guide:start", guide);
2606
3497
  this.notify();
@@ -2660,10 +3551,7 @@ var GuidelyClient = class {
2660
3551
  const steps = [];
2661
3552
  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) });
2662
3553
  const checklists = [];
2663
- this.checklists.forEach((w, id) => {
2664
- const guide = this.findGuide(id);
2665
- if (guide) checklists.push({ guide, progress: this.getChecklistProgress(id), open: w.open });
2666
- });
3554
+ this.checklists.forEach((w, id) => checklists.push({ guide: w.guide, progress: this.getChecklistProgress(id), open: w.open }));
2667
3555
  this.ui = { steps, checklists, survey: this.surveyView, resourceCenterOpen: this.resourceCenterOpen, theme: this.themeFor(), messenger: !!this.messenger };
2668
3556
  for (const cb of Array.from(this.uiListeners)) {
2669
3557
  try {
@@ -2721,7 +3609,7 @@ var GuidelyClient = class {
2721
3609
  progress.dismissedAt = nowIso();
2722
3610
  this.persistState();
2723
3611
  this.enqueue("checklist_dismissed", { guideId: guide.id, guideVersion: guide.version });
2724
- 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 });
3612
+ 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 });
2725
3613
  this.checklists.delete(guide.id);
2726
3614
  this.notify();
2727
3615
  }
@@ -2734,7 +3622,7 @@ var GuidelyClient = class {
2734
3622
  this.persistState();
2735
3623
  this.recordEnd(guide, "completed");
2736
3624
  this.enqueue("survey_submitted", { guideId: guide.id, guideVersion: guide.version, properties: { responseId } });
2737
- 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 });
3625
+ 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 });
2738
3626
  this.emitter.emit("survey:submit", { guide, answers, responseId });
2739
3627
  this.emitter.emit("guide:complete", guide);
2740
3628
  this.surveyView = { ...this.surveyView, submitted: true };
@@ -2748,10 +3636,58 @@ var GuidelyClient = class {
2748
3636
  getAudienceIds() {
2749
3637
  return this.state.audienceIds;
2750
3638
  }
3639
+ /** The published resource center in the visitor's language, or null when it is off. */
2751
3640
  getResourceCenter() {
2752
- const rc = this.config?.resourceCenter;
3641
+ const rc = this.localizedResourceCenter();
2753
3642
  return rc && rc.enabled ? rc : null;
2754
3643
  }
3644
+ /** The resource center in the visitor's language (cached until the config or language changes). */
3645
+ localizedResourceCenter() {
3646
+ const rc = this.config?.resourceCenter;
3647
+ if (!rc) return null;
3648
+ const locales = this.locales();
3649
+ const key = `${locales.join(",")}|${this.defaultLocale()}`;
3650
+ if (this.localizedRc?.source !== rc || this.localizedRc.locales !== key) this.localizedRc = { source: rc, locales: key, value: localizeResourceCenter(rc, locales, this.defaultLocale()) };
3651
+ return this.localizedRc.value;
3652
+ }
3653
+ /** Evaluates targeting rules (e.g. a resource-center section's) for the current visitor and screen. */
3654
+ matchesTargeting(rules) {
3655
+ if (!rules) return true;
3656
+ try {
3657
+ return evaluateRules(rules, this.getContext());
3658
+ } catch (err) {
3659
+ this.reportError(err);
3660
+ return false;
3661
+ }
3662
+ }
3663
+ /** Resource-center sections this visitor sees, in order (disabled and non-matching ones left out). */
3664
+ getResourceCenterModules() {
3665
+ const rc = this.getResourceCenter();
3666
+ if (!rc) return [];
3667
+ return orderedModules(rc.modules).filter((m) => m.enabled && (!("targeting" in m) || this.matchesTargeting(m.targeting))).map((m) => m.id);
3668
+ }
3669
+ /**
3670
+ * Sections of the messenger's Help tab, in order: the resource center's modules without "What's
3671
+ * new" (the messenger has its own News tab); every section when no resource center is published.
3672
+ */
3673
+ getHelpModules() {
3674
+ const modules = this.getResourceCenter() ? this.getResourceCenterModules() : [...RC_MODULE_IDS];
3675
+ return modules.filter((id) => id !== "announcements");
3676
+ }
3677
+ /** "What's new" entries this visitor sees (newest first); empty when the section is off. */
3678
+ getAnnouncements() {
3679
+ const rc = this.getResourceCenter();
3680
+ if (!rc || !this.getResourceCenterModules().includes("announcements")) return [];
3681
+ return visibleAnnouncements(rc.announcements, this.state.audienceIds);
3682
+ }
3683
+ /** "What's new" entries published since the visitor last opened the resource center. */
3684
+ getUnreadAnnouncements() {
3685
+ return unreadAnnouncements(this.getAnnouncements(), this.state.announcementsSeenAt);
3686
+ }
3687
+ /** Whether an entry is new to the visitor in the resource center that is open now. */
3688
+ isAnnouncementNew(a) {
3689
+ return unreadAnnouncements([a], this.announcementsSeenAtOpen) > 0;
3690
+ }
2755
3691
  async searchResourceCenter(q) {
2756
3692
  try {
2757
3693
  const res = await this.api.get("/resource-center/search", { params: { q } });
@@ -2760,6 +3696,11 @@ var GuidelyClient = class {
2760
3696
  return [];
2761
3697
  }
2762
3698
  }
3699
+ /** The language of the resource center as shown, when it isn't the default (sent with article fetches). */
3700
+ articleLocale() {
3701
+ const locale = this.localizedResourceCenter()?.locale;
3702
+ return locale && locale.toLowerCase() !== this.defaultLocale().toLowerCase() ? locale : void 0;
3703
+ }
2763
3704
  articleViewed(article) {
2764
3705
  this.enqueue("article_viewed", { properties: { articleId: article.id, slug: article.slug } });
2765
3706
  }
@@ -2791,7 +3732,7 @@ var GuidelyClient = class {
2791
3732
  this.enqueue("checklist_completed", { guideId: guide.id, guideVersion: guide.version });
2792
3733
  }
2793
3734
  this.persistState();
2794
- 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 });
3735
+ 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 });
2795
3736
  this.emitter.emit("checklist:task", { guide, task, progress: this.getChecklistProgress(guide.id) });
2796
3737
  if (complete) {
2797
3738
  this.emitter.emit("checklist:complete", guide);
@@ -2894,6 +3835,11 @@ var GuidelyClient = class {
2894
3835
  }
2895
3836
  if (this.resourceCenterOpen) return;
2896
3837
  this.resourceCenterOpen = true;
3838
+ this.announcementsSeenAtOpen = this.state.announcementsSeenAt ?? null;
3839
+ if (this.getAnnouncements().length) {
3840
+ this.state.announcementsSeenAt = nowIso();
3841
+ this.persistState();
3842
+ }
2897
3843
  this.enqueue("resource_center_opened");
2898
3844
  this.emitter.emit("resource-center:open");
2899
3845
  this.notify();
@@ -2928,7 +3874,9 @@ var GuidelyClient = class {
2928
3874
  },
2929
3875
  onMessage: (message, conversation) => this.emitter.emit("conversation:message", { message, conversation }),
2930
3876
  onStartGuide: (key) => this.setTimer(() => this.startGuide(key), 0),
2931
- onError: (err) => this.log("messenger", err instanceof Error ? err.message : err)
3877
+ onError: (err) => this.log("messenger", err instanceof Error ? err.message : err),
3878
+ health: this.api.health,
3879
+ locale: () => this.articleLocale()
2932
3880
  });
2933
3881
  this.resourceCenterOpen = false;
2934
3882
  }
@@ -3033,6 +3981,7 @@ var GuidelyClient = class {
3033
3981
  audienceIds: this.state.audienceIds,
3034
3982
  session: this.session,
3035
3983
  pendingEvents: this.queue?.size ?? 0,
3984
+ pendingWrites: this.outbox?.size ?? 0,
3036
3985
  targets: this.targets.ids(),
3037
3986
  flagsReady: this.flags.isReady()
3038
3987
  };
@@ -3061,7 +4010,191 @@ var GuidelyClient = class {
3061
4010
  }
3062
4011
  };
3063
4012
  function forPlatform(config, platform) {
3064
- return config.guides.every((g) => supportsPlatform(g.config, platform)) ? config : { ...config, guides: config.guides.filter((g) => supportsPlatform(g.config, platform)) };
4013
+ const keep = (g) => supportsPlatform(g.config, platform);
4014
+ const holdouts = config.guideHoldouts?.filter((h) => keep(h.guide)).map((h) => ({ ...h, guide: withoutCode(h.guide) }));
4015
+ return { ...config, guides: config.guides.filter(keep).map(withoutCode), ...holdouts ? { guideHoldouts: holdouts } : {} };
4016
+ }
4017
+ function withoutCode(guide) {
4018
+ if (!guide.config.steps.some((s) => s.code)) return guide;
4019
+ return { ...guide, config: { ...guide.config, steps: guide.config.steps.map(({ code: _code, ...step }) => step) } };
4020
+ }
4021
+
4022
+ // src/ui/Autocapture.tsx
4023
+ import { useMemo as useMemo5, useRef as useRef5 } from "react";
4024
+ import { View as View10 } from "react-native";
4025
+
4026
+ // src/core/viewTree.ts
4027
+ var HOST_TEXT = 6;
4028
+ var MAX_ANCESTORS = 500;
4029
+ var MAX_TEXT_NODES = 300;
4030
+ var INTERACTIVE_ROLES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem", "checkbox", "switch", "radio", "togglebutton", "imagebutton", "option", "combobox", "adjustable"]);
4031
+ var TEXT_INPUT_HOST = /TextInput/i;
4032
+ var NOISE = /^(Anonymous|Unknown|ForwardRef|Memo|Context|Provider|Consumer|Fragment|StrictMode|Suspense|Profiler)$|Context$|Provider$|Consumer$/;
4033
+ var isFiber = (v) => !!v && typeof v === "object" && "return" in v && "memoizedProps" in v;
4034
+ function fiberFromEvent(event) {
4035
+ try {
4036
+ const e = event;
4037
+ if (isFiber(e?._targetInst)) return e._targetInst;
4038
+ const target = e?.target;
4039
+ if (!target || typeof target !== "object") return null;
4040
+ for (const key of ["__internalInstanceHandle", "_internalInstanceHandle", "_internalFiberInstanceHandleDEV"]) if (isFiber(target[key])) return target[key];
4041
+ for (const sym of Object.getOwnPropertySymbols(target)) if (sym.description === "internalInstanceHandle" && isFiber(target[sym])) return target[sym];
4042
+ } catch {
4043
+ }
4044
+ return null;
4045
+ }
4046
+ function componentName(type, depth = 0) {
4047
+ if (!type || depth > 3) return null;
4048
+ if (typeof type === "function") {
4049
+ const f = type;
4050
+ const n = typeof f.displayName === "string" && f.displayName ? f.displayName : typeof f.name === "string" ? f.name : "";
4051
+ return n || null;
4052
+ }
4053
+ if (typeof type === "object") {
4054
+ const o = type;
4055
+ if (typeof o.displayName === "string" && o.displayName) return o.displayName;
4056
+ return componentName(o.render, depth + 1) ?? componentName(o.type, depth + 1);
4057
+ }
4058
+ return null;
4059
+ }
4060
+ var isHost = (f) => typeof f.type === "string";
4061
+ var isTextInput = (f) => typeof f.type === "string" ? TEXT_INPUT_HOST.test(f.type) : componentName(f.type) === "TextInput";
4062
+ function isInteractive(props) {
4063
+ if (!props || typeof props !== "object") return null;
4064
+ if (typeof props.onPress === "function" || typeof props.onLongPress === "function") return "press";
4065
+ const role = props.accessibilityRole ?? props.role;
4066
+ return typeof role === "string" && INTERACTIVE_ROLES.has(role) ? "role" : null;
4067
+ }
4068
+ function firstHost(f) {
4069
+ let cur = f;
4070
+ for (let i = 0; cur && i < 30; i++, cur = cur.child) if (isHost(cur)) return cur;
4071
+ return null;
4072
+ }
4073
+ function idSelector(f) {
4074
+ const host = firstHost(f);
4075
+ const sources = [f.memoizedProps, host?.memoizedProps];
4076
+ for (const key of ["nativeID", "testID"]) {
4077
+ for (const p of sources) {
4078
+ const v = p && typeof p === "object" ? p[key] : void 0;
4079
+ if (typeof v === "string" && v) return attrSelector(key, v);
4080
+ }
4081
+ }
4082
+ return null;
4083
+ }
4084
+ function pathSelector(f) {
4085
+ const parts = [];
4086
+ let cur = f;
4087
+ for (let i = 0; cur && parts.length < 4 && i < MAX_ANCESTORS; i++, cur = cur.return) {
4088
+ if (isHost(cur)) continue;
4089
+ const name = componentName(cur.type);
4090
+ if (!name || NOISE.test(name)) continue;
4091
+ let same = 0;
4092
+ let index = 0;
4093
+ for (let s = cur.return?.child, n = 0; s && n < 500; s = s.sibling, n++) {
4094
+ if (s.type !== cur.type) continue;
4095
+ same += 1;
4096
+ if (s === cur) index = same;
4097
+ }
4098
+ parts.unshift(same > 1 && index ? `${name}:nth-of-type(${index})` : name);
4099
+ }
4100
+ return parts.join(" > ").slice(0, 300) || "View";
4101
+ }
4102
+ function subtreeText(f, opts) {
4103
+ const out = [];
4104
+ let visited = 0;
4105
+ let masked = false;
4106
+ const walk = (node) => {
4107
+ for (let n = node; n && visited < MAX_TEXT_NODES && !masked; n = n.sibling) {
4108
+ visited += 1;
4109
+ if (opts.isMask?.(n.type)) {
4110
+ masked = true;
4111
+ return;
4112
+ }
4113
+ if (isTextInput(n)) continue;
4114
+ if (n.tag === HOST_TEXT && (typeof n.memoizedProps === "string" || typeof n.memoizedProps === "number")) out.push(String(n.memoizedProps));
4115
+ else walk(n.child);
4116
+ }
4117
+ };
4118
+ if (opts.isMask?.(f.type)) return { text: "", masked: true };
4119
+ walk(f.child);
4120
+ return { text: out.join(" "), masked };
4121
+ }
4122
+ function measureHost(host) {
4123
+ return new Promise((resolve) => {
4124
+ let settled = false;
4125
+ const done = (r) => {
4126
+ if (settled) return;
4127
+ settled = true;
4128
+ clearTimeout(timer);
4129
+ resolve(r);
4130
+ };
4131
+ const timer = setTimeout(() => done(null), 500);
4132
+ const cb = (x, y, width, height) => done(Number.isFinite(x) && Number.isFinite(y) && width > 0 && height > 0 ? { x, y, width, height } : null);
4133
+ try {
4134
+ const sn = host.stateNode;
4135
+ const pub = sn?.canonical?.publicInstance ?? sn;
4136
+ if (pub && typeof pub.measureInWindow === "function") return pub.measureInWindow(cb);
4137
+ const ui = globalThis.nativeFabricUIManager;
4138
+ if (sn?.node && typeof ui?.measureInWindow === "function") return ui.measureInWindow(sn.node, cb);
4139
+ } catch {
4140
+ }
4141
+ done(null);
4142
+ });
4143
+ }
4144
+ function describe(f, masked, opts, withText) {
4145
+ const props = f.memoizedProps && typeof f.memoizedProps === "object" ? f.memoizedProps : {};
4146
+ const host = firstHost(f);
4147
+ const el = { selector: idSelector(f) ?? pathSelector(f), tag: componentName(f.type) ?? "View" };
4148
+ if (withText && !masked) {
4149
+ const sub = subtreeText(f, opts);
4150
+ if (!sub.masked) {
4151
+ const hostProps = host?.memoizedProps && typeof host.memoizedProps === "object" ? host.memoizedProps : {};
4152
+ const label = props.accessibilityLabel ?? props["aria-label"] ?? hostProps.accessibilityLabel ?? hostProps["aria-label"];
4153
+ const text = cleanText(sub.text) ?? (typeof label === "string" ? cleanText(label) : void 0);
4154
+ if (text) el.text = text;
4155
+ }
4156
+ }
4157
+ if (host) el.measure = () => measureHost(host);
4158
+ return el;
4159
+ }
4160
+ function resolveFiber(target, opts = {}) {
4161
+ const none = { ignored: false, element: null, hit: null };
4162
+ if (!target) return none;
4163
+ try {
4164
+ let press = null;
4165
+ let role = null;
4166
+ let hitFiber = null;
4167
+ let stopped = false;
4168
+ let masked = false;
4169
+ for (let f = target, i = 0; f && i < MAX_ANCESTORS; f = f.return, i++) {
4170
+ if (opts.isIgnored?.(f.type)) return { ignored: true, element: null, hit: null };
4171
+ if (opts.isMask?.(f.type)) masked = true;
4172
+ if (stopped) continue;
4173
+ if (isTextInput(f)) {
4174
+ stopped = true;
4175
+ continue;
4176
+ }
4177
+ if (isHost(f)) continue;
4178
+ const name = componentName(f.type);
4179
+ if (!hitFiber && name && !NOISE.test(name)) hitFiber = f;
4180
+ const kind = isInteractive(f.memoizedProps);
4181
+ if (kind === "press") {
4182
+ press = f;
4183
+ stopped = true;
4184
+ } else if (kind === "role" && !role) role = f;
4185
+ }
4186
+ const element = press ?? role;
4187
+ return {
4188
+ ignored: false,
4189
+ element: element ? describe(element, masked, opts, true) : null,
4190
+ hit: hitFiber ? describe(hitFiber, masked, opts, componentName(hitFiber.type) === "Text") : null
4191
+ };
4192
+ } catch {
4193
+ return none;
4194
+ }
4195
+ }
4196
+ function resolveTapTarget(event, opts = {}) {
4197
+ return resolveFiber(fiberFromEvent(event), opts);
3065
4198
  }
3066
4199
 
3067
4200
  // src/ui/Overlay.tsx
@@ -3585,9 +4718,9 @@ var styles3 = StyleSheet3.create({
3585
4718
  });
3586
4719
 
3587
4720
  // src/ui/Messenger.tsx
3588
- import { useCallback, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2, useState as useState3, useSyncExternalStore as useSyncExternalStore2 } from "react";
4721
+ import { Fragment as Fragment2, useCallback, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2, useState as useState3, useSyncExternalStore as useSyncExternalStore2 } from "react";
3589
4722
  import { ActivityIndicator, Keyboard, Modal as Modal2, Platform as Platform2, Pressable as Pressable4, ScrollView as ScrollView3, StyleSheet as StyleSheet4, Text as Text4, TextInput, View as View4 } from "react-native";
3590
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
4723
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
3591
4724
  function useMessenger(store) {
3592
4725
  return useSyncExternalStore2(
3593
4726
  useCallback((cb) => store.subscribe(cb), [store]),
@@ -3821,25 +4954,41 @@ function Help({ store, client, palette: p }) {
3821
4954
  ] }, title);
3822
4955
  const audiences = client.getAudienceIds();
3823
4956
  const sections = resourceCenterSections(articles?.collections ?? [], articles?.articles ?? [], audiences);
3824
- const listed = results?.filter((a) => isArticleListed(articles?.collections ?? [], a, audiences));
4957
+ const modules = client.getHelpModules();
4958
+ const searchOn = modules.includes("search");
4959
+ const listed = searchOn ? results?.filter((a) => isArticleListed(articles?.collections ?? [], a, audiences)) : void 0;
4960
+ const checklists = (rc?.checklistGuideIds ?? []).map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g && g.kind === "checklist");
4961
+ const renderModule = (id) => {
4962
+ switch (id) {
4963
+ case "guides":
4964
+ return recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u25B6"))) : null;
4965
+ case "checklists":
4966
+ return checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u2611"))) : null;
4967
+ case "articles":
4968
+ return /* @__PURE__ */ jsxs3(Fragment3, { children: [
4969
+ !articles ? /* @__PURE__ */ jsx5(ActivityIndicator, { color: p.muted }) : null,
4970
+ sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => open(a)))))
4971
+ ] });
4972
+ case "faqs":
4973
+ return rc?.faqs.length ? section(
4974
+ "FAQ",
4975
+ rc.faqs.map((f) => /* @__PURE__ */ jsxs3(View4, { style: [styles4.row, styles4.faq, { borderColor: p.border }], children: [
4976
+ /* @__PURE__ */ jsxs3(Pressable4, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles4.faqHead, children: [
4977
+ /* @__PURE__ */ jsx5(Text4, { style: [styles4.rowTitle, styles4.flex, { color: p.text }], children: f.question }),
4978
+ /* @__PURE__ */ jsx5(Text4, { style: [styles4.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
4979
+ ] }),
4980
+ faq === f.id ? /* @__PURE__ */ jsx5(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
4981
+ ] }, f.id))
4982
+ ) : null;
4983
+ case "links":
4984
+ return rc?.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null;
4985
+ default:
4986
+ return null;
4987
+ }
4988
+ };
3825
4989
  return /* @__PURE__ */ jsxs3(ScrollView3, { contentContainerStyle: styles4.content, keyboardShouldPersistTaps: "handled", children: [
3826
- /* @__PURE__ */ jsx5(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) }] }),
3827
- listed ? section(`Results for \u201C${q.trim()}\u201D`, listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => open(a))) : /* @__PURE__ */ jsx5(Text4, { style: [styles4.rowSub, { color: p.muted }], children: "No articles found." })) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
3828
- recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u25B6"))) : null,
3829
- !articles ? /* @__PURE__ */ jsx5(ActivityIndicator, { color: p.muted }) : null,
3830
- sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => open(a))))),
3831
- rc?.faqs.length ? section(
3832
- "FAQ",
3833
- rc.faqs.map((f) => /* @__PURE__ */ jsxs3(View4, { style: [styles4.row, styles4.faq, { borderColor: p.border }], children: [
3834
- /* @__PURE__ */ jsxs3(Pressable4, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles4.faqHead, children: [
3835
- /* @__PURE__ */ jsx5(Text4, { style: [styles4.rowTitle, styles4.flex, { color: p.text }], children: f.question }),
3836
- /* @__PURE__ */ jsx5(Text4, { style: [styles4.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
3837
- ] }),
3838
- faq === f.id ? /* @__PURE__ */ jsx5(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
3839
- ] }, f.id))
3840
- ) : null,
3841
- rc?.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null
3842
- ] })
4990
+ searchOn ? /* @__PURE__ */ jsx5(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,
4991
+ listed ? section(`Results for \u201C${q.trim()}\u201D`, listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => open(a))) : /* @__PURE__ */ jsx5(Text4, { style: [styles4.rowSub, { color: p.muted }], children: "No articles found." })) : /* @__PURE__ */ jsx5(Fragment3, { children: modules.filter((id) => id !== "search").map((id) => /* @__PURE__ */ jsx5(Fragment2, { children: renderModule(id) }, id)) })
3843
4992
  ] });
3844
4993
  }
3845
4994
  function Article({ article, palette: p, client }) {
@@ -3906,9 +5055,9 @@ var styles4 = StyleSheet4.create({
3906
5055
  });
3907
5056
 
3908
5057
  // src/ui/ResourceCenter.tsx
3909
- import { useEffect as useEffect4, useMemo as useMemo4, useState as useState4 } from "react";
5058
+ import { Fragment as Fragment4, useEffect as useEffect4, useMemo as useMemo4, useState as useState4 } from "react";
3910
5059
  import { Modal as Modal3, Platform as Platform3, Pressable as Pressable5, ScrollView as ScrollView4, StyleSheet as StyleSheet5, Text as Text5, TextInput as TextInput2, View as View5 } from "react-native";
3911
- import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
5060
+ import { Fragment as Fragment5, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
3912
5061
  function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
3913
5062
  const rc = client.getResourceCenter();
3914
5063
  const [query, setQuery] = useState4("");
@@ -3942,6 +5091,8 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
3942
5091
  const checklists = useMemo4(() => (rc?.checklistGuideIds ?? []).map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g && g.kind === "checklist"), [rc, guides]);
3943
5092
  if (!rc) return null;
3944
5093
  const close = () => client.closeResourceCenter();
5094
+ const modules = client.getResourceCenterModules();
5095
+ const announcements = client.getAnnouncements();
3945
5096
  const openArticle = (a) => {
3946
5097
  if (a.externalUrl) return client.navigate(a.externalUrl);
3947
5098
  client.articleViewed(a);
@@ -3960,7 +5111,51 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
3960
5111
  ] }, title);
3961
5112
  const audiences = client.getAudienceIds();
3962
5113
  const sections = resourceCenterSections(rc.collections, rc.articles, audiences);
3963
- const listed = results?.filter((a) => isArticleListed(rc.collections, a, audiences));
5114
+ const listed = modules.includes("search") ? results?.filter((a) => isArticleListed(rc.collections, a, audiences)) : void 0;
5115
+ const link = (label, onPress) => /* @__PURE__ */ jsx6(Pressable5, { accessibilityRole: "button", accessibilityLabel: label, onPress, hitSlop: 8, children: /* @__PURE__ */ jsx6(Text5, { style: [styles5.link, { color: p.primary }], children: label }) }, label);
5116
+ const announcement2 = (a) => {
5117
+ const linked = a.articleId ? rc.articles.find((x) => x.id === a.articleId) : void 0;
5118
+ const isNew = client.isAnnouncementNew(a);
5119
+ return /* @__PURE__ */ jsxs4(View5, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
5120
+ /* @__PURE__ */ jsxs4(View5, { style: styles5.faqHead, children: [
5121
+ /* @__PURE__ */ jsx6(Text5, { accessibilityRole: "header", style: [styles5.rowTitle, styles5.flex, { color: p.text, fontFamily: p.fontFamily }], children: a.title }),
5122
+ isNew ? /* @__PURE__ */ jsx6(Text5, { style: [styles5.badge, { color: p.onPrimary, backgroundColor: p.primary }], children: "NEW" }) : null
5123
+ ] }),
5124
+ a.body ? /* @__PURE__ */ jsx6(RichText, { html: a.body, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null,
5125
+ a.guideId || linked || a.url ? /* @__PURE__ */ jsxs4(View5, { style: styles5.links, children: [
5126
+ a.guideId ? link("Show me", () => (close(), client.startGuide(a.guideId))) : null,
5127
+ linked ? link("Read more", () => openArticle(linked)) : null,
5128
+ a.url ? link(a.linkLabel || "Learn more", () => client.navigate(a.url)) : null
5129
+ ] }) : null
5130
+ ] }, a.id);
5131
+ };
5132
+ const renderModule = (id) => {
5133
+ switch (id) {
5134
+ case "announcements":
5135
+ return announcements.length ? section("What's new", announcements.map(announcement2)) : null;
5136
+ case "guides":
5137
+ return recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => (close(), client.startGuide(g.id)), "\u25B6"))) : null;
5138
+ case "checklists":
5139
+ return checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => (close(), client.openChecklist(g.id)), "\u2611"))) : null;
5140
+ case "articles":
5141
+ 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)))));
5142
+ case "faqs":
5143
+ return rc.faqs.length ? section(
5144
+ "FAQ",
5145
+ rc.faqs.map((f) => /* @__PURE__ */ jsxs4(View5, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
5146
+ /* @__PURE__ */ jsxs4(Pressable5, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles5.faqHead, children: [
5147
+ /* @__PURE__ */ jsx6(Text5, { style: [styles5.rowTitle, styles5.flex, { color: p.text }], children: f.question }),
5148
+ /* @__PURE__ */ jsx6(Text5, { style: [styles5.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
5149
+ ] }),
5150
+ faq === f.id ? /* @__PURE__ */ jsx6(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
5151
+ ] }, f.id))
5152
+ ) : null;
5153
+ case "links":
5154
+ return rc.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null;
5155
+ default:
5156
+ return null;
5157
+ }
5158
+ };
3964
5159
  return /* @__PURE__ */ jsx6(Modal3, { visible: open, animationType: "slide", presentationStyle: Platform3.OS === "ios" ? "pageSheet" : "fullScreen", onRequestClose: () => article ? setArticle(null) : close(), children: /* @__PURE__ */ jsxs4(View5, { style: [styles5.sheet, { backgroundColor: p.background, paddingTop: Platform3.OS === "ios" ? 8 : topInset }], children: [
3965
5160
  /* @__PURE__ */ jsxs4(View5, { style: [styles5.header, { borderColor: p.border }], children: [
3966
5161
  article ? /* @__PURE__ */ jsx6(Pressable5, { accessibilityRole: "button", accessibilityLabel: "Back", onPress: () => setArticle(null), hitSlop: 10, children: /* @__PURE__ */ jsx6(Text5, { style: [styles5.back, { color: p.primary }], children: "\u2039 Back" }) }) : null,
@@ -3971,7 +5166,7 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
3971
5166
  /* @__PURE__ */ jsx6(Text5, { accessibilityRole: "header", style: [styles5.articleTitle, { color: p.text, fontFamily: p.fontFamily }], children: article.title }),
3972
5167
  /* @__PURE__ */ jsx6(RichText, { html: article.body, palette: p, onLink: (u) => client.navigate(u) })
3973
5168
  ] }) : /* @__PURE__ */ jsxs4(ScrollView4, { contentContainerStyle: [styles5.content, { paddingBottom: bottomInset + 24 }], keyboardShouldPersistTaps: "handled", children: [
3974
- /* @__PURE__ */ jsx6(
5169
+ modules.includes("search") ? /* @__PURE__ */ jsx6(
3975
5170
  TextInput2,
3976
5171
  {
3977
5172
  value: query,
@@ -3983,26 +5178,11 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
3983
5178
  returnKeyType: "search",
3984
5179
  style: [styles5.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }]
3985
5180
  }
3986
- ),
5181
+ ) : null,
3987
5182
  listed ? section(
3988
5183
  `Results for \u201C${query.trim()}\u201D`,
3989
5184
  listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => openArticle(a))) : /* @__PURE__ */ jsx6(Text5, { style: [styles5.rowSub, { color: p.muted }], children: "No articles found." })
3990
- ) : /* @__PURE__ */ jsxs4(Fragment3, { children: [
3991
- recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => (close(), client.startGuide(g.id)), "\u25B6"))) : null,
3992
- checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => (close(), client.openChecklist(g.id)), "\u2611"))) : null,
3993
- sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => openArticle(a))))),
3994
- rc.faqs.length ? section(
3995
- "FAQ",
3996
- rc.faqs.map((f) => /* @__PURE__ */ jsxs4(View5, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
3997
- /* @__PURE__ */ jsxs4(Pressable5, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles5.faqHead, children: [
3998
- /* @__PURE__ */ jsx6(Text5, { style: [styles5.rowTitle, styles5.flex, { color: p.text }], children: f.question }),
3999
- /* @__PURE__ */ jsx6(Text5, { style: [styles5.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
4000
- ] }),
4001
- faq === f.id ? /* @__PURE__ */ jsx6(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
4002
- ] }, f.id))
4003
- ) : null,
4004
- rc.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null
4005
- ] })
5185
+ ) : /* @__PURE__ */ jsx6(Fragment5, { children: modules.filter((id) => id !== "search").map((id) => /* @__PURE__ */ jsx6(Fragment4, { children: renderModule(id) }, id)) })
4006
5186
  ] })
4007
5187
  ] }) });
4008
5188
  }
@@ -4022,7 +5202,10 @@ var styles5 = StyleSheet5.create({
4022
5202
  chev: { fontSize: 18 },
4023
5203
  faq: { flexDirection: "column", alignItems: "stretch" },
4024
5204
  faqHead: { flexDirection: "row", alignItems: "center", gap: 10 },
4025
- articleTitle: { fontSize: 22, fontWeight: "700" }
5205
+ articleTitle: { fontSize: 22, fontWeight: "700" },
5206
+ badge: { fontSize: 10, fontWeight: "800", letterSpacing: 0.5, paddingHorizontal: 6, paddingVertical: 2, borderRadius: 6, overflow: "hidden" },
5207
+ links: { flexDirection: "row", flexWrap: "wrap", gap: 16, marginTop: 4 },
5208
+ link: { fontSize: 14, fontWeight: "600" }
4026
5209
  });
4027
5210
 
4028
5211
  // src/ui/StepLayer.tsx
@@ -4076,7 +5259,19 @@ function StepCard({ view, client, palette: p, width, style, compact }) {
4076
5259
  const showProgress = step.showProgress && view.total > 1;
4077
5260
  const onLink = (url) => client.navigate(url);
4078
5261
  return /* @__PURE__ */ jsx7(Surface, { palette: p, style: [{ width }, style], children: /* @__PURE__ */ jsxs5(View6, { accessible: false, accessibilityViewIsModal: view.asModal || step.type === "modal", style: compact ? styles6.compact : styles6.inner, children: [
4079
- step.imageUrl ? /* @__PURE__ */ jsx7(Image2, { source: { uri: step.imageUrl }, style: [styles6.image, { borderRadius: Math.max(0, p.radius - 4) }], resizeMode: "cover", accessibilityIgnoresInvertColors: true }) : null,
5262
+ step.imageUrl ? (
5263
+ // An empty description marks the image as decorative: screen readers skip it.
5264
+ /* @__PURE__ */ jsx7(
5265
+ Image2,
5266
+ {
5267
+ source: { uri: step.imageUrl },
5268
+ style: [styles6.image, { borderRadius: Math.max(0, p.radius - 4) }],
5269
+ resizeMode: "cover",
5270
+ accessibilityIgnoresInvertColors: true,
5271
+ ...step.imageAlt?.trim() ? { accessible: true, accessibilityRole: "image", accessibilityLabel: step.imageAlt.trim() } : { accessible: false, importantForAccessibility: "no" }
5272
+ }
5273
+ )
5274
+ ) : null,
4080
5275
  step.title ? /* @__PURE__ */ jsxs5(View6, { style: styles6.header, children: [
4081
5276
  /* @__PURE__ */ jsx7(Text6, { accessibilityRole: "header", style: [styles6.title, { color: p.text, fontFamily: p.fontFamily }], children: step.title }),
4082
5277
  step.showClose ? /* @__PURE__ */ jsx7(CloseButton, { palette: p, onPress: () => client.closeStep(guide.id), label: "Dismiss guide" }) : null
@@ -4674,18 +5869,22 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
4674
5869
  const basePalette = palette(ui.theme, scheme);
4675
5870
  const messenger = ui.messenger ? client.getMessenger() : null;
4676
5871
  const showRcLauncher = !messenger && !!client.options.showResourceCenterLauncher && !!client.getResourceCenter();
5872
+ const unread = showRcLauncher ? client.getUnreadAnnouncements() : 0;
4677
5873
  const messengerLeft = messenger?.getSnapshot().config.launcher.position === "bottom-left";
4678
5874
  const checklistOnSameSide = ui.checklists.some((c) => c.guide.config.checklist?.position === "bottom-left" === messengerLeft) && client.options.showChecklistLauncher !== false;
4679
5875
  return /* @__PURE__ */ jsxs8(View9, { ref, collapsable: false, pointerEvents: "box-none", style: StyleSheet9.absoluteFill, onLayout: measureOrigin, children: [
4680
5876
  ui.checklists.map((c) => /* @__PURE__ */ jsx10(Checklist, { view: c, client, palette: palette(client.themeFor(c.guide), scheme), geometry, showLauncher: client.options.showChecklistLauncher !== false, launcherBottom: launcherOffset }, c.guide.id)),
4681
- showRcLauncher ? /* @__PURE__ */ jsx10(
5877
+ showRcLauncher ? /* @__PURE__ */ jsxs8(
4682
5878
  Pressable8,
4683
5879
  {
4684
5880
  accessibilityRole: "button",
4685
- accessibilityLabel: client.getResourceCenter()?.launcher.label || "Help",
5881
+ accessibilityLabel: rcLauncherLabel(client),
4686
5882
  onPress: () => client.openResourceCenter(),
4687
5883
  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 }],
4688
- children: /* @__PURE__ */ jsx10(Text8, { style: [styles9.rcText, { color: basePalette.onPrimary }], children: "?" })
5884
+ children: [
5885
+ /* @__PURE__ */ jsx10(Text8, { style: [styles9.rcText, { color: basePalette.onPrimary }], children: "?" }),
5886
+ unread ? /* @__PURE__ */ jsx10(View9, { pointerEvents: "none", style: [styles9.rcBadge, { borderColor: basePalette.primary }], children: /* @__PURE__ */ jsx10(Text8, { style: styles9.rcBadgeText, children: unread > 9 ? "9+" : unread }) }) : null
5887
+ ]
4689
5888
  }
4690
5889
  ) : null,
4691
5890
  messenger && client.options.showMessengerLauncher !== false ? /* @__PURE__ */ jsx10(MessengerLauncher, { store: messenger, palette: basePalette, style: [messengerLeft ? { left: insets.left + 16 } : { right: insets.right + 16 }, { bottom: insets.bottom + launcherOffset + (checklistOnSameSide ? 56 : 0) }] }) : null,
@@ -4696,27 +5895,75 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
4696
5895
  }
4697
5896
  var styles9 = StyleSheet9.create({
4698
5897
  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 } },
4699
- rcText: { fontSize: 22, fontWeight: "800" }
5898
+ rcText: { fontSize: 22, fontWeight: "800" },
5899
+ rcBadge: { position: "absolute", top: -4, right: -4, minWidth: 20, height: 20, paddingHorizontal: 5, borderRadius: 10, borderWidth: 2, backgroundColor: "#dc2626", alignItems: "center", justifyContent: "center" },
5900
+ rcBadgeText: { color: "#ffffff", fontSize: 11, fontWeight: "800" }
4700
5901
  });
5902
+ function rcLauncherLabel(client) {
5903
+ const label = client.getResourceCenter()?.launcher.label || "Help";
5904
+ const unread = client.getUnreadAnnouncements();
5905
+ return unread ? `${label} (${unread} new update${unread === 1 ? "" : "s"})` : label;
5906
+ }
4701
5907
 
4702
- // src/ui/Target.tsx
4703
- import { createContext as createContext2, useCallback as useCallback3, useContext as useContext2, useEffect as useEffect8, useRef as useRef5 } from "react";
4704
- import { View as View10 } from "react-native";
5908
+ // src/ui/Autocapture.tsx
4705
5909
  import { jsx as jsx11 } from "react/jsx-runtime";
5910
+ function GuidelyMask({ children, ...rest }) {
5911
+ return /* @__PURE__ */ jsx11(View10, { ...rest, children });
5912
+ }
5913
+ var RESOLVE = {
5914
+ isIgnored: (type) => type === GuidelyOverlay,
5915
+ isMask: (type) => type === GuidelyMask
5916
+ };
5917
+ function useTapObserver(client) {
5918
+ const clientRef = useRef5(client);
5919
+ clientRef.current = client;
5920
+ return useMemo5(() => {
5921
+ const tracker = new TapTracker();
5922
+ const guard = (fn) => (e) => {
5923
+ try {
5924
+ fn(e);
5925
+ } catch {
5926
+ tracker.cancel();
5927
+ }
5928
+ };
5929
+ return {
5930
+ onTouchStartCapture: guard((e) => {
5931
+ const n = e.nativeEvent;
5932
+ if (!clientRef.current?.isTapCaptureOn()) return tracker.cancel();
5933
+ tracker.begin(n.pageX, n.pageY, Date.now(), Array.isArray(n.touches) ? n.touches.length : 1, n.identifier);
5934
+ }),
5935
+ onTouchMoveCapture: guard((e) => tracker.move(e.nativeEvent.pageX, e.nativeEvent.pageY, e.nativeEvent.identifier)),
5936
+ onTouchEndCapture: guard((e) => {
5937
+ const n = e.nativeEvent;
5938
+ const tap = tracker.end(n.pageX, n.pageY, Date.now(), n.identifier);
5939
+ const c = clientRef.current;
5940
+ if (!tap || !c) return;
5941
+ const r = resolveTapTarget(e, RESOLVE);
5942
+ if (!r.ignored) c.recordTap({ x: tap.x, y: tap.y, t: Date.now(), element: r.element, hit: r.hit });
5943
+ }),
5944
+ onTouchCancelCapture: guard(() => tracker.cancel())
5945
+ };
5946
+ }, []);
5947
+ }
5948
+
5949
+ // src/ui/Target.tsx
5950
+ import { createContext as createContext2, useCallback as useCallback3, useContext as useContext2, useEffect as useEffect8, useRef as useRef6 } from "react";
5951
+ import { View as View11 } from "react-native";
5952
+ import { jsx as jsx12 } from "react/jsx-runtime";
4706
5953
  var GuidelyContext = createContext2(null);
4707
5954
  var GuidelyScreenContext = createContext2(null);
4708
5955
  function GuidelyScreenScope({ name, children }) {
4709
- return /* @__PURE__ */ jsx11(GuidelyScreenContext.Provider, { value: name, children });
5956
+ return /* @__PURE__ */ jsx12(GuidelyScreenContext.Provider, { value: name, children });
4710
5957
  }
4711
5958
  function guidelyScreenLayout({ route, children }) {
4712
- return /* @__PURE__ */ jsx11(GuidelyScreenScope, { name: route.name, children });
5959
+ return /* @__PURE__ */ jsx12(GuidelyScreenScope, { name: route.name, children });
4713
5960
  }
4714
- var TAP_SLOP = 10;
5961
+ var TAP_SLOP2 = 10;
4715
5962
  function useGuidelyTarget(id) {
4716
5963
  const client = useContext2(GuidelyContext);
4717
5964
  const screen = useContext2(GuidelyScreenContext);
4718
- const ref = useRef5(null);
4719
- const start = useRef5(null);
5965
+ const ref = useRef6(null);
5966
+ const start = useRef6(null);
4720
5967
  useEffect8(() => {
4721
5968
  if (!client || !id) return;
4722
5969
  return client.targets.register(id, {
@@ -4736,7 +5983,7 @@ function useGuidelyTarget(id) {
4736
5983
  (e) => {
4737
5984
  const s = start.current;
4738
5985
  start.current = null;
4739
- if (s && Math.abs(e.nativeEvent.pageX - s.x) < TAP_SLOP && Math.abs(e.nativeEvent.pageY - s.y) < TAP_SLOP) client?.targets.tapped(id);
5986
+ if (s && Math.abs(e.nativeEvent.pageX - s.x) < TAP_SLOP2 && Math.abs(e.nativeEvent.pageY - s.y) < TAP_SLOP2) client?.targets.tapped(id);
4740
5987
  },
4741
5988
  [client, id]
4742
5989
  );
@@ -4744,8 +5991,8 @@ function useGuidelyTarget(id) {
4744
5991
  }
4745
5992
  function GuidelyTarget({ id, children, onLayout, onTouchStart, onTouchEnd, ...rest }) {
4746
5993
  const t = useGuidelyTarget(id);
4747
- return /* @__PURE__ */ jsx11(
4748
- View10,
5994
+ return /* @__PURE__ */ jsx12(
5995
+ View11,
4749
5996
  {
4750
5997
  ...rest,
4751
5998
  ref: t.ref,
@@ -4769,7 +6016,7 @@ function GuidelyTarget({ id, children, onLayout, onTouchStart, onTouchEnd, ...re
4769
6016
 
4770
6017
  // src/index.tsx
4771
6018
  import { createFlagsClient as createFlagsClient3, InMemoryStickyBucketStore } from "@guidelytech/flags";
4772
- import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
6019
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
4773
6020
  function deviceLocale() {
4774
6021
  try {
4775
6022
  return Intl.DateTimeFormat().resolvedOptions().locale;
@@ -4787,21 +6034,22 @@ function defaultBridge(appVersion) {
4787
6034
  appVersion
4788
6035
  },
4789
6036
  openExternal: (url) => void Linking.openURL(url).catch(() => {
4790
- })
6037
+ }),
6038
+ windowSize: () => Dimensions.get("window")
4791
6039
  };
4792
6040
  }
4793
6041
  function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPayload, flagAttributes, ...options }) {
4794
6042
  const [client, setClient] = useState8(null);
4795
- const optionsRef = useRef6(options);
6043
+ const optionsRef = useRef7(options);
4796
6044
  optionsRef.current = options;
4797
- const userRef = useRef6(user);
6045
+ const userRef = useRef7(user);
4798
6046
  userRef.current = user;
4799
6047
  const flagsOpts = options.flags === false ? null : options.flags ?? {};
4800
6048
  const payload = flagsPayload ?? flagsOpts?.payload ?? null;
4801
- const payloadRef = useRef6(payload);
6049
+ const payloadRef = useRef7(payload);
4802
6050
  payloadRef.current = payload;
4803
- const appAttributes = useMemo5(() => ({ ...flagsOpts?.attributes ?? {}, ...flagAttributes ?? {} }), [stableStringify2(flagsOpts?.attributes ?? {}), stableStringify2(flagAttributes ?? {})]);
4804
- const appAttributesRef = useRef6(appAttributes);
6051
+ const appAttributes = useMemo6(() => ({ ...flagsOpts?.attributes ?? {}, ...flagAttributes ?? {} }), [stableStringify3(flagsOpts?.attributes ?? {}), stableStringify3(flagAttributes ?? {})]);
6052
+ const appAttributesRef = useRef7(appAttributes);
4805
6053
  appAttributesRef.current = appAttributes;
4806
6054
  const { projectId, environment, apiUrl, storage } = options;
4807
6055
  useEffect9(() => {
@@ -4838,7 +6086,7 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
4838
6086
  };
4839
6087
  }, [projectId, environment, apiUrl, storage]);
4840
6088
  const userKey = user ? JSON.stringify([user.id, user.traits ?? null, user.account ?? null]) : "";
4841
- const hadUser = useRef6(false);
6089
+ const hadUser = useRef7(false);
4842
6090
  useEffect9(() => {
4843
6091
  if (!client) return;
4844
6092
  if (user && user.id) {
@@ -4852,13 +6100,18 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
4852
6100
  useEffect9(() => {
4853
6101
  client?.flags.setAttributes(appAttributes);
4854
6102
  }, [client, appAttributes]);
6103
+ useEffect9(() => {
6104
+ client?.setLocale(options.locale ?? null);
6105
+ }, [client, options.locale]);
4855
6106
  const bootstrap = useBootstrap(payload, { ...user?.id ? { ...user.traits ?? {}, id: user.id } : {}, ...appAttributes });
4856
- const flags = useMemo5(() => ({ client: client?.flags ?? null, bootstrap }), [client, bootstrap]);
4857
- return /* @__PURE__ */ jsx12(GuidelyContext.Provider, { value: client, children: /* @__PURE__ */ jsxs9(FlagsContext.Provider, { value: flags, children: [
4858
- children,
4859
- client ? /* @__PURE__ */ jsx12(GuidelyOverlay, { client, insets, launcherOffset }) : null
6107
+ const flags = useMemo6(() => ({ client: client?.flags ?? null, bootstrap }), [client, bootstrap]);
6108
+ const tapObserver = useTapObserver(client);
6109
+ return /* @__PURE__ */ jsx13(GuidelyContext.Provider, { value: client, children: /* @__PURE__ */ jsxs9(FlagsContext.Provider, { value: flags, children: [
6110
+ options.autocapture === false ? children : /* @__PURE__ */ jsx13(View12, { style: styles10.root, ...tapObserver, children }),
6111
+ client ? /* @__PURE__ */ jsx13(GuidelyOverlay, { client, insets, launcherOffset }) : null
4860
6112
  ] }) });
4861
6113
  }
6114
+ var styles10 = StyleSheet10.create({ root: { flex: 1 } });
4862
6115
  var NOOP_RESULTS = {
4863
6116
  init: Promise.resolve(),
4864
6117
  ready: Promise.resolve(),
@@ -4930,7 +6183,7 @@ function useGuidelyIdentify(userId, traits, account) {
4930
6183
  }
4931
6184
  function useGuidelyEvent(event, handler) {
4932
6185
  const client = useContext3(GuidelyContext);
4933
- const ref = useRef6(handler);
6186
+ const ref = useRef7(handler);
4934
6187
  ref.current = handler;
4935
6188
  useEffect9(() => {
4936
6189
  if (!client) return;
@@ -4945,7 +6198,7 @@ function useGuidelyScreen(name, properties) {
4945
6198
  }
4946
6199
  function useGuidelyNavigation(navigationRef, options = {}) {
4947
6200
  const client = useContext3(GuidelyContext);
4948
- const nameRef = useRef6(options.screenName);
6201
+ const nameRef = useRef7(options.screenName);
4949
6202
  nameRef.current = options.screenName;
4950
6203
  const report = () => {
4951
6204
  const route = navigationRef.getCurrentRoute?.();
@@ -4956,7 +6209,7 @@ function useGuidelyNavigation(navigationRef, options = {}) {
4956
6209
  }
4957
6210
  function useUiSelector(select, fallback) {
4958
6211
  const client = useContext3(GuidelyContext);
4959
- const selectRef = useRef6(select);
6212
+ const selectRef = useRef7(select);
4960
6213
  selectRef.current = select;
4961
6214
  const [value, setValue] = useState8(() => client ? select(client) : fallback);
4962
6215
  useEffect9(() => {
@@ -4998,14 +6251,18 @@ export {
4998
6251
  FeatureValue,
4999
6252
  FlagsProvider,
5000
6253
  GuidelyClient,
6254
+ GuidelyMask,
5001
6255
  GuidelyOverlay,
5002
6256
  GuidelyProvider,
5003
6257
  GuidelyScreenScope,
5004
6258
  GuidelyTarget,
6259
+ HeatmapBuffer,
5005
6260
  InMemoryStickyBucketStore,
5006
6261
  Messenger,
6262
+ RageClickDetector,
5007
6263
  SDK_NAME,
5008
6264
  SDK_VERSION,
6265
+ TapTracker,
5009
6266
  TargetRegistry,
5010
6267
  createFlagsClient3 as createFlagsClient,
5011
6268
  createSseParser,