@guidelytech/react-native 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -28,14 +28,18 @@ __export(index_exports, {
28
28
  FeatureValue: () => FeatureValue,
29
29
  FlagsProvider: () => FlagsProvider,
30
30
  GuidelyClient: () => GuidelyClient,
31
+ GuidelyMask: () => GuidelyMask,
31
32
  GuidelyOverlay: () => GuidelyOverlay,
32
33
  GuidelyProvider: () => GuidelyProvider,
33
34
  GuidelyScreenScope: () => GuidelyScreenScope,
34
35
  GuidelyTarget: () => GuidelyTarget,
36
+ HeatmapBuffer: () => HeatmapBuffer,
35
37
  InMemoryStickyBucketStore: () => import_flags6.InMemoryStickyBucketStore,
36
38
  Messenger: () => Messenger,
39
+ RageClickDetector: () => RageClickDetector,
37
40
  SDK_NAME: () => SDK_NAME,
38
41
  SDK_VERSION: () => SDK_VERSION,
42
+ TapTracker: () => TapTracker,
39
43
  TargetRegistry: () => TargetRegistry,
40
44
  createFlagsClient: () => import_flags6.createFlagsClient,
41
45
  createSseParser: () => createSseParser,
@@ -63,8 +67,8 @@ __export(index_exports, {
63
67
  useGuidelyTarget: () => useGuidelyTarget
64
68
  });
65
69
  module.exports = __toCommonJS(index_exports);
66
- var import_react9 = require("react");
67
- var import_react_native12 = require("react-native");
70
+ var import_react10 = require("react");
71
+ var import_react_native13 = require("react-native");
68
72
  var import_flags3 = require("@guidelytech/flags");
69
73
 
70
74
  // src/flags.tsx
@@ -156,7 +160,7 @@ function FlagsProvider({ children, client: external, payload, ...options }) {
156
160
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FlagsContext.Provider, { value, children });
157
161
  }
158
162
 
159
- // ../shared/dist/chunk-UW6NRYRY.js
163
+ // ../shared/dist/chunk-TGIUEYXM.js
160
164
  function fnv1a(input) {
161
165
  let h = 2166136261;
162
166
  for (let i = 0; i < input.length; i++) {
@@ -336,6 +340,8 @@ function resolveField(rule, ctx) {
336
340
  return ctx.now.getHours();
337
341
  case "rollout.percentage":
338
342
  return rolloutBucket(ctx.userId || ctx.anonymousId, ctx.rolloutSalt || "guidely");
343
+ case "experiment.bucket":
344
+ return rule.key ? rolloutBucket(ctx.userId || ctx.anonymousId, rule.key) : void 0;
339
345
  case "environment":
340
346
  return ctx.environment;
341
347
  case "locale":
@@ -346,6 +352,9 @@ function resolveField(rule, ctx) {
346
352
  return ctx.screen;
347
353
  case "app.version":
348
354
  return ctx.appVersion;
355
+ case "page.views":
356
+ case "feature.clicks":
357
+ return rule.key ? ctx.tagUsage?.[rule.key] ?? 0 : 0;
349
358
  default:
350
359
  return void 0;
351
360
  }
@@ -454,8 +463,8 @@ function sanitizeProperties(props) {
454
463
  for (const [k, v] of Object.entries(props)) {
455
464
  if (SENSITIVE_PROPERTY_PATTERN.test(k)) continue;
456
465
  if (typeof v === "function" || typeof v === "symbol") continue;
457
- const str = typeof v === "string" ? v : JSON.stringify(v ?? null);
458
- size += k.length + (str?.length ?? 0);
466
+ const str2 = typeof v === "string" ? v : JSON.stringify(v ?? null);
467
+ size += k.length + (str2?.length ?? 0);
459
468
  if (size > MAX_PROPERTY_BYTES) break;
460
469
  out[k.slice(0, 100)] = typeof v === "string" ? v.slice(0, 1e3) : v;
461
470
  }
@@ -612,9 +621,151 @@ function isArticleListed(collections, article, member) {
612
621
  }
613
622
  return true;
614
623
  }
624
+ var RC_MODULE_IDS = ["announcements", "search", "checklists", "guides", "articles", "links", "faqs"];
625
+ function orderedModules(modules) {
626
+ const known = (modules ?? []).filter((m, i, all) => RC_MODULE_IDS.includes(m.id) && all.findIndex((x) => x.id === m.id) === i);
627
+ const missing2 = RC_MODULE_IDS.filter((id) => !known.some((m) => m.id === id)).map((id) => ({ id, enabled: true }));
628
+ return [...known, ...missing2];
629
+ }
630
+ function visibleAnnouncements(items, member, now = Date.now()) {
631
+ return (items ?? []).filter((a) => {
632
+ const t = Date.parse(a.publishedAt);
633
+ return !Number.isNaN(t) && t <= now && matchesAudiences(a.audienceIds, member);
634
+ }).sort((a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt));
635
+ }
636
+ function unreadAnnouncements(items, lastSeenAt) {
637
+ const seen = lastSeenAt ? Date.parse(lastSeenAt) : NaN;
638
+ if (Number.isNaN(seen)) return items.length;
639
+ return items.filter((a) => Date.parse(a.publishedAt) > seen).length;
640
+ }
615
641
  var CAPTURE_SCREENSHOT_MAX_BYTES = 8 * 1024 * 1024;
642
+ var DEFAULT_LOCALE = "en";
643
+ function normalizeLocale(input) {
644
+ if (typeof input !== "string") return "";
645
+ const parts = input.trim().replace(/_/g, "-").split("-").filter(Boolean);
646
+ if (!parts.length || !/^[a-zA-Z]{2,3}$/.test(parts[0])) return "";
647
+ return parts.slice(0, 3).map((p, i) => i === 0 ? p.toLowerCase() : p.length === 4 ? p[0].toUpperCase() + p.slice(1).toLowerCase() : p.length === 2 ? p.toUpperCase() : p).join("-");
648
+ }
649
+ function baseLanguage(locale) {
650
+ return normalizeLocale(locale).split("-")[0] ?? "";
651
+ }
652
+ function resolveLocale(requested, available, defaultLocale = DEFAULT_LOCALE) {
653
+ const def = normalizeLocale(defaultLocale) || DEFAULT_LOCALE;
654
+ const avail = available.map(normalizeLocale).filter(Boolean);
655
+ const all = [def, ...avail.filter((a) => a.toLowerCase() !== def.toLowerCase())];
656
+ for (const raw of requested) {
657
+ const want = normalizeLocale(raw);
658
+ if (!want) continue;
659
+ const exact = all.find((a) => a.toLowerCase() === want.toLowerCase());
660
+ if (exact) return exact;
661
+ const base = baseLanguage(want);
662
+ const sameBase = all.find((a) => a.toLowerCase() === base) ?? all.find((a) => baseLanguage(a) === base);
663
+ if (sameBase) return sameBase;
664
+ }
665
+ return def;
666
+ }
667
+ function visitorLocales(explicit, traits, platform = []) {
668
+ const out = [];
669
+ const push = (v) => {
670
+ const n = normalizeLocale(v);
671
+ if (n && !out.includes(n)) out.push(n);
672
+ };
673
+ push(explicit);
674
+ push(traits?.language);
675
+ push(traits?.locale);
676
+ for (const p of platform) push(p);
677
+ return out;
678
+ }
679
+ function guideLocales(config) {
680
+ const t = config.localization?.translations ?? {};
681
+ return Object.keys(t).filter((l) => Object.values(t[l] ?? {}).some((v) => typeof v === "string" && v.trim() !== ""));
682
+ }
683
+ function pick(map, key, fallback) {
684
+ const v = map?.[key];
685
+ return typeof v === "string" && v.trim() !== "" ? v : fallback;
686
+ }
687
+ function applyGuideTranslations(config, map) {
688
+ if (!map || !Object.keys(map).length) return config;
689
+ return {
690
+ ...config,
691
+ steps: config.steps.map((s) => ({
692
+ ...s,
693
+ title: pick(map, `steps.${s.id}.title`, s.title),
694
+ body: pick(map, `steps.${s.id}.body`, s.body),
695
+ customHtml: pick(map, `steps.${s.id}.customHtml`, s.customHtml),
696
+ imageAlt: pick(map, `steps.${s.id}.imageAlt`, s.imageAlt),
697
+ buttons: s.buttons.map((b) => ({ ...b, label: pick(map, `steps.${s.id}.buttons.${b.id}.label`, b.label) }))
698
+ })),
699
+ checklist: config.checklist ? {
700
+ ...config.checklist,
701
+ launcherLabel: pick(map, "checklist.launcherLabel", config.checklist.launcherLabel),
702
+ completionTitle: pick(map, "checklist.completionTitle", config.checklist.completionTitle),
703
+ completionBody: pick(map, "checklist.completionBody", config.checklist.completionBody),
704
+ tasks: config.checklist.tasks.map((t) => ({ ...t, title: pick(map, `checklist.tasks.${t.id}.title`, t.title), description: pick(map, `checklist.tasks.${t.id}.description`, t.description) }))
705
+ } : config.checklist,
706
+ survey: config.survey ? {
707
+ ...config.survey,
708
+ thankYouTitle: pick(map, "survey.thankYouTitle", config.survey.thankYouTitle),
709
+ thankYouBody: pick(map, "survey.thankYouBody", config.survey.thankYouBody),
710
+ questions: config.survey.questions.map((q) => ({
711
+ ...q,
712
+ title: pick(map, `survey.questions.${q.id}.title`, q.title),
713
+ description: pick(map, `survey.questions.${q.id}.description`, q.description),
714
+ placeholder: pick(map, `survey.questions.${q.id}.placeholder`, q.placeholder),
715
+ lowLabel: pick(map, `survey.questions.${q.id}.lowLabel`, q.lowLabel),
716
+ highLabel: pick(map, `survey.questions.${q.id}.highLabel`, q.highLabel),
717
+ options: q.options.map((o) => ({ ...o, label: pick(map, `survey.questions.${q.id}.options.${o.id}.label`, o.label) }))
718
+ }))
719
+ } : config.survey
720
+ };
721
+ }
722
+ function localizeGuide(guide, requested, defaultLocale = DEFAULT_LOCALE) {
723
+ const translations = guide.config.localization?.translations ?? {};
724
+ const locale = resolveLocale(requested, guideLocales(guide.config), defaultLocale);
725
+ const key = Object.keys(translations).find((l) => normalizeLocale(l).toLowerCase() === locale.toLowerCase());
726
+ if (!key) return { ...guide, locale };
727
+ return { ...guide, locale, config: applyGuideTranslations(guide.config, translations[key]) };
728
+ }
729
+ function resourceCenterLocales(rc) {
730
+ const set = new Set(Object.keys(rc.translations ?? {}));
731
+ for (const a of rc.articles) for (const l of Object.keys(a.translations ?? {})) set.add(l);
732
+ for (const a of rc.announcements ?? []) for (const l of Object.keys(a.translations ?? {})) set.add(l);
733
+ return [...set];
734
+ }
735
+ function translationFor(map, locale) {
736
+ if (!map) return void 0;
737
+ const key = Object.keys(map).find((l) => normalizeLocale(l).toLowerCase() === locale.toLowerCase());
738
+ return key ? map[key] : void 0;
739
+ }
740
+ function localizeArticle(a, locale) {
741
+ const t = translationFor(a.translations, locale);
742
+ if (!t) return a;
743
+ const body = t.body?.trim() ? t.body : a.body;
744
+ return { ...a, title: t.title?.trim() ? t.title : a.title, summary: t.summary?.trim() ? t.summary : a.summary, body, ...t.bodyOmitted ? { bodyOmitted: true } : {} };
745
+ }
746
+ function localizeAnnouncement(a, locale) {
747
+ const t = translationFor(a.translations, locale);
748
+ if (!t) return a;
749
+ return { ...a, title: t.title?.trim() ? t.title : a.title, body: t.body?.trim() ? t.body : a.body };
750
+ }
751
+ function localizeResourceCenter(rc, requested, defaultLocale = DEFAULT_LOCALE) {
752
+ const locale = resolveLocale(requested, resourceCenterLocales(rc), defaultLocale);
753
+ if (locale.toLowerCase() === normalizeLocale(defaultLocale).toLowerCase()) return { ...rc, locale };
754
+ const map = translationFor(rc.translations, locale);
755
+ return {
756
+ ...rc,
757
+ locale,
758
+ title: pick(map, "title", rc.title),
759
+ launcher: { ...rc.launcher, label: pick(map, "launcher.label", rc.launcher.label) },
760
+ collections: rc.collections.map((c) => ({ ...c, name: pick(map, `collections.${c.id}.name`, c.name), description: pick(map, `collections.${c.id}.description`, c.description) })),
761
+ links: rc.links.map((l) => ({ ...l, label: pick(map, `links.${l.id}.label`, l.label) })),
762
+ faqs: rc.faqs.map((f) => ({ ...f, question: pick(map, `faqs.${f.id}.question`, f.question), answer: pick(map, `faqs.${f.id}.answer`, f.answer) })),
763
+ articles: rc.articles.map((a) => localizeArticle(a, locale)),
764
+ announcements: rc.announcements?.map((a) => localizeAnnouncement(a, locale))
765
+ };
766
+ }
616
767
 
617
- // ../sdk-core/dist/chunk-C4WWSF67.js
768
+ // ../sdk-core/dist/chunk-JUSM7J5N.js
618
769
  var __defProp2 = Object.defineProperty;
619
770
  var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
620
771
  var __publicField2 = (obj, key, value) => __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -912,6 +1063,8 @@ var KEYS = {
912
1063
  queue: (key) => `guidely:queue:${key}`,
913
1064
  /** Survey answers and guide/checklist progress not yet accepted by the server. */
914
1065
  outbox: (key) => `guidely:outbox:${key}`,
1066
+ /** Agent Analytics turns not yet accepted by the server. */
1067
+ agentOutbox: (key) => `guidely:agents:${key}`,
915
1068
  session: "guidely:session",
916
1069
  active: "guidely:active",
917
1070
  authoring: "guidely:authoring",
@@ -924,7 +1077,7 @@ var KEYS = {
924
1077
  newsSeen: (key) => `guidely:news:${key}`
925
1078
  };
926
1079
 
927
- // ../sdk-core/dist/chunk-GDJB466J.js
1080
+ // ../sdk-core/dist/chunk-ZAF2KRJA.js
928
1081
  var EventQueue = class {
929
1082
  constructor(opts) {
930
1083
  __publicField2(this, "opts", opts);
@@ -1200,12 +1353,32 @@ function areMutuallyExclusive(a, b) {
1200
1353
  return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
1201
1354
  }
1202
1355
  function isWithinSchedule(guide, now = Date.now()) {
1203
- const s = guide.config.survey;
1204
- if (!s) return true;
1205
- if (s.startAt && now < Date.parse(s.startAt)) return false;
1206
- if (s.endAt && now > Date.parse(s.endAt)) return false;
1356
+ for (const w of [guide.config.schedule, guide.config.survey]) {
1357
+ if (!w) continue;
1358
+ if (w.startAt && now < Date.parse(w.startAt)) return false;
1359
+ if (w.endAt && now > Date.parse(w.endAt)) return false;
1360
+ }
1361
+ return true;
1362
+ }
1363
+ var MINUTE = 60 * 1e3;
1364
+ function countsTowardThrottle(guide) {
1365
+ return isExclusiveGuide(guide) && !guide.config.frequency.ignoreThrottling;
1366
+ }
1367
+ function isThrottleEligible(settings, launches, now = Date.now()) {
1368
+ if (!settings || !settings.enabled) return true;
1369
+ const times = launches.map((l) => Date.parse(l)).filter((t) => !Number.isNaN(t));
1370
+ const windowMs = Math.max(1, settings.windowHours) * HOUR;
1371
+ if (times.filter((t) => now - t < windowMs).length >= Math.max(1, settings.maxGuides)) return false;
1372
+ if (settings.minGapMinutes > 0 && times.some((t) => now - t < settings.minGapMinutes * MINUTE)) return false;
1207
1373
  return true;
1208
1374
  }
1375
+ function pruneLaunches(launches, settings, now = Date.now()) {
1376
+ const keepMs = Math.max(settings?.windowHours ?? 24, 1) * HOUR + (settings?.minGapMinutes ?? 0) * MINUTE;
1377
+ return launches.filter((l) => {
1378
+ const t = Date.parse(l);
1379
+ return !Number.isNaN(t) && now - t < keepMs;
1380
+ }).slice(-100);
1381
+ }
1209
1382
  function dismissedAfterMerge(local, server) {
1210
1383
  if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
1211
1384
  return local?.dismissedAt ?? server.dismissedAt ?? null;
@@ -1214,6 +1387,115 @@ function dismissedAfterMerge(local, server) {
1214
1387
  // src/core/client.ts
1215
1388
  var import_flags2 = require("@guidelytech/flags");
1216
1389
 
1390
+ // src/core/autocapture.ts
1391
+ var TAP_SLOP = 10;
1392
+ var TAP_MAX_MS = 1e3;
1393
+ var HEATMAP_MAX_POINTS = 40;
1394
+ function quoteValue(value) {
1395
+ return `"${value.replace(/["\\]/g, "\\$&")}"`;
1396
+ }
1397
+ function attrSelector(name, value) {
1398
+ return `[${name}=${quoteValue(value)}]`.slice(0, 300);
1399
+ }
1400
+ function cleanText(text) {
1401
+ const t = (text ?? "").replace(/\s+/g, " ").trim();
1402
+ return t ? t.slice(0, 100) : void 0;
1403
+ }
1404
+ function tapProperties(el, x, y, platform, maskAll) {
1405
+ const props = { selector: el.selector.slice(0, 300), tag: el.tag || "View", x: Math.round(x) || 0, y: Math.round(y) || 0, platform };
1406
+ const text = maskAll ? void 0 : cleanText(el.text);
1407
+ if (text) props.text = text;
1408
+ return props;
1409
+ }
1410
+ var r2 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 100) / 100;
1411
+ function heatmapPoint(x, y, el, rect, rage) {
1412
+ const p = { k: rage ? "r" : "c", x: Math.round(x) || 0, y: Math.round(y) || 0 };
1413
+ const s = el?.selector.slice(0, 120);
1414
+ if (s) p.s = s;
1415
+ if (s && rect && rect.width > 0 && rect.height > 0) {
1416
+ p.rx = r2((x - rect.x) / rect.width);
1417
+ p.ry = r2((y - rect.y) / rect.height);
1418
+ }
1419
+ return p;
1420
+ }
1421
+ var RageClickDetector = class {
1422
+ constructor(threshold = 3, windowMs = 1e3, radius = 30) {
1423
+ __publicField(this, "threshold", threshold);
1424
+ __publicField(this, "windowMs", windowMs);
1425
+ __publicField(this, "radius", radius);
1426
+ __publicField(this, "clicks", []);
1427
+ __publicField(this, "fired", false);
1428
+ }
1429
+ /** Taps in the current burst once it has fired (rage), otherwise 0. */
1430
+ get burst() {
1431
+ return this.fired ? this.clicks.length : 0;
1432
+ }
1433
+ /** Records a tap; returns the tap count when it completes a burst, otherwise 0. */
1434
+ add(x, y, t) {
1435
+ const first = this.clicks[0];
1436
+ const last = this.clicks[this.clicks.length - 1];
1437
+ if (!first || !last || t - last.t > this.windowMs || Math.hypot(x - first.x, y - first.y) > this.radius) {
1438
+ this.clicks = [];
1439
+ this.fired = false;
1440
+ }
1441
+ this.clicks = this.clicks.filter((c) => t - c.t <= this.windowMs);
1442
+ this.clicks.push({ x, y, t });
1443
+ if (this.fired || this.clicks.length < this.threshold) return 0;
1444
+ this.fired = true;
1445
+ return this.clicks.length;
1446
+ }
1447
+ };
1448
+ var HeatmapBuffer = class {
1449
+ constructor() {
1450
+ /** Screen path the buffered points belong to. */
1451
+ __publicField(this, "url", "");
1452
+ __publicField(this, "points", []);
1453
+ }
1454
+ /** Adds a tap (`burst` > 0 marks it and the burst's previous taps as rage); true when the batch is full. */
1455
+ add(url, point, burst) {
1456
+ this.url = url;
1457
+ for (let i = Math.max(0, this.points.length - burst + 1); i < this.points.length; i++) this.points[i].k = "r";
1458
+ this.points.push(point);
1459
+ return this.points.length >= HEATMAP_MAX_POINTS;
1460
+ }
1461
+ /** Empties the buffer, returning what was in it. */
1462
+ take() {
1463
+ const points = this.points;
1464
+ this.points = [];
1465
+ return points.length ? { url: this.url, points } : null;
1466
+ }
1467
+ };
1468
+ var TapTracker = class {
1469
+ constructor() {
1470
+ __publicField(this, "start", null);
1471
+ __publicField(this, "moved", 0);
1472
+ }
1473
+ /** A finger went down. A second finger while one is tracked cancels the gesture (not a tap). */
1474
+ begin(x, y, t, touches, id) {
1475
+ if (this.start || touches > 1) {
1476
+ this.start = null;
1477
+ return;
1478
+ }
1479
+ this.start = { x, y, t, id };
1480
+ this.moved = 0;
1481
+ }
1482
+ move(x, y, id) {
1483
+ const s = this.start;
1484
+ if (s && (id === void 0 || s.id === void 0 || id === s.id)) this.moved = Math.max(this.moved, Math.hypot(x - s.x, y - s.y));
1485
+ }
1486
+ /** The finger lifted: the tap position when it moved ≤ 10pt and lasted < 1s, else null. */
1487
+ end(x, y, t, id) {
1488
+ const s = this.start;
1489
+ if (!s || id !== void 0 && s.id !== void 0 && id !== s.id) return null;
1490
+ this.start = null;
1491
+ const moved = Math.max(this.moved, Math.hypot(x - s.x, y - s.y));
1492
+ return moved <= TAP_SLOP && t - s.t < TAP_MAX_MS ? { x, y } : null;
1493
+ }
1494
+ cancel() {
1495
+ this.start = null;
1496
+ }
1497
+ };
1498
+
1217
1499
  // src/core/targets.ts
1218
1500
  var TargetTimeoutError = class extends Error {
1219
1501
  constructor(ids) {
@@ -1316,6 +1598,35 @@ var TargetRegistry = class {
1316
1598
  return null;
1317
1599
  }
1318
1600
  }
1601
+ /**
1602
+ * The registered target on the current screen whose visible frame contains the point (window
1603
+ * coordinates); the smallest one when targets are nested. Frames that do not answer within
1604
+ * `timeoutMs` are skipped.
1605
+ */
1606
+ async hitTest(x, y, timeoutMs = 500) {
1607
+ const candidates = [];
1608
+ this.targets.forEach((list, id) => {
1609
+ for (const handle of list) if (!(handle.screen && this.screen && handle.screen !== this.screen)) candidates.push({ id, handle });
1610
+ });
1611
+ if (!candidates.length) return null;
1612
+ const rects = await Promise.all(
1613
+ candidates.map(
1614
+ ({ handle }) => new Promise((resolve) => {
1615
+ const timer = setTimeout(() => resolve(null), timeoutMs);
1616
+ void this.measureHandle(handle).then((r) => {
1617
+ clearTimeout(timer);
1618
+ resolve(r);
1619
+ });
1620
+ })
1621
+ )
1622
+ );
1623
+ let best = null;
1624
+ rects.forEach((r, i) => {
1625
+ if (!r || x < r.x || y < r.y || x > r.x + r.width || y > r.y + r.height) return;
1626
+ if (!best || r.width * r.height < best.rect.width * best.rect.height) best = { id: candidates[i].id, rect: r };
1627
+ });
1628
+ return best;
1629
+ }
1319
1630
  /** First of `ids` that is mounted and visible. */
1320
1631
  async resolve(ids) {
1321
1632
  for (const id of ids) {
@@ -2073,7 +2384,8 @@ var Messenger = class {
2073
2384
  this.stopThreadPolling();
2074
2385
  this.set({ open: true, view: { kind: "article", articleId: id, from }, article: null });
2075
2386
  try {
2076
- const res = await this.request("GET", `/articles/${encodeURIComponent(id)}`);
2387
+ const locale = this.deps.locale?.();
2388
+ const res = await this.request("GET", `/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : {});
2077
2389
  this.set({ article: res.article });
2078
2390
  } catch (err) {
2079
2391
  this.deps.onError(err);
@@ -2188,7 +2500,7 @@ var PersistentStore = class {
2188
2500
 
2189
2501
  // src/core/client.ts
2190
2502
  var SDK_NAME = "@guidelytech/react-native";
2191
- var SDK_VERSION = "0.10.0";
2503
+ var SDK_VERSION = "0.11.0";
2192
2504
  var DEFAULT_API_URL = "https://guidelytech.com";
2193
2505
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
2194
2506
  var CACHED_START_MS = 1500;
@@ -2202,6 +2514,8 @@ function settlesWithin(promise, ms) {
2202
2514
  promise.then(done, done);
2203
2515
  });
2204
2516
  }
2517
+ var appVersionKey = (key) => `guidely:app:${key}`;
2518
+ var UNKNOWN_VIEW = { selector: "View", tag: "View" };
2205
2519
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
2206
2520
  var emptyState = () => ({ guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] });
2207
2521
  var newSession = () => ({ id: `s_${randomId(16)}`, startedAt: nowIso(), lastActiveAt: nowIso(), shownGuides: [], events: [] });
@@ -2233,6 +2547,7 @@ var GuidelyClient = class {
2233
2547
  __publicField(this, "activeSurvey", null);
2234
2548
  __publicField(this, "queued", []);
2235
2549
  __publicField(this, "flows", /* @__PURE__ */ new Map());
2550
+ /** Checklists on screen, with the guide as rendered (in the visitor's language). */
2236
2551
  __publicField(this, "checklists", /* @__PURE__ */ new Map());
2237
2552
  __publicField(this, "expanded", /* @__PURE__ */ new Set());
2238
2553
  __publicField(this, "surveyView", null);
@@ -2252,6 +2567,27 @@ var GuidelyClient = class {
2252
2567
  __publicField(this, "flags");
2253
2568
  /** Experiments already reported this session (`experiment_viewed` is sent once per session). */
2254
2569
  __publicField(this, "exposures", /* @__PURE__ */ new Set());
2570
+ // Product analytics (autocapture).
2571
+ __publicField(this, "rage", new RageClickDetector());
2572
+ __publicField(this, "heatmap", new HeatmapBuffer());
2573
+ /** Taps are processed in order (target frames are measured asynchronously). */
2574
+ __publicField(this, "tapChain", Promise.resolve());
2575
+ /** When the current screen's foreground measurement started; `screenLeft` once it has been reported. */
2576
+ __publicField(this, "screenStartedAt", 0);
2577
+ __publicField(this, "screenLeft", true);
2578
+ /** Last `app_open` (cold start or return to the foreground), for `app_background.foregroundMs`. */
2579
+ __publicField(this, "openedAt", Date.now());
2580
+ __publicField(this, "launchChecked", false);
2581
+ /** An identity was stored before this launch (an existing install adopting lifecycle events). */
2582
+ __publicField(this, "hadIdentity", false);
2583
+ /** Language set with `setLocale()` (or the `locale` option). */
2584
+ __publicField(this, "locale", null);
2585
+ /** Language each shown guide was rendered in; added to its events as `locale`. */
2586
+ __publicField(this, "guideLocales", /* @__PURE__ */ new Map());
2587
+ /** The resource center in the visitor's language, recomputed when the config or language changes. */
2588
+ __publicField(this, "localizedRc", null);
2589
+ /** "What's new" last-seen time when the resource center opened (entries after it read as new). */
2590
+ __publicField(this, "announcementsSeenAtOpen", null);
2255
2591
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
2256
2592
  __publicField(this, "eventBus", {
2257
2593
  on: (name, cb) => {
@@ -2281,6 +2617,7 @@ var GuidelyClient = class {
2281
2617
  if (!options.storage) this.log("no `storage` passed: identity, guide history, queued events and survey answers last only for this app session, so whatever could not be sent during a Guidely outage is lost when the app restarts. Pass AsyncStorage to persist them.");
2282
2618
  this.cleanups.push(this.targets.onTap((id) => this.handleTap(id)));
2283
2619
  if (options.user) this.user = { ...options.user };
2620
+ this.locale = options.locale ? normalizeLocale(options.locale) || null : null;
2284
2621
  this.flags = this.createFlags(options.flags);
2285
2622
  }
2286
2623
  createFlags(opts) {
@@ -2329,9 +2666,10 @@ var GuidelyClient = class {
2329
2666
  async bootstrap() {
2330
2667
  const key = this.options.projectId;
2331
2668
  try {
2332
- 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)]);
2669
+ await this.store.hydrate([KEYS.anon, KEYS.seen, KEYS.session, KEYS.config(key), KEYS.etag(key), KEYS.state(key), KEYS.user(key), KEYS.queue(key), KEYS.outbox(key), KEYS.conversations(key), appVersionKey(key)]);
2333
2670
  if (this.destroyed) return;
2334
2671
  this.anonymousId = this.store.get(KEYS.anon) || "";
2672
+ this.hadIdentity = !!this.anonymousId;
2335
2673
  if (!this.anonymousId) {
2336
2674
  this.anonymousId = `anon_${randomId(20)}`;
2337
2675
  this.store.set(KEYS.anon, this.anonymousId);
@@ -2368,6 +2706,7 @@ var GuidelyClient = class {
2368
2706
  if (!this.config) await fresh;
2369
2707
  else if (!await settlesWithin(fresh, CACHED_START_MS)) void fresh.then(() => this.applyConfigChange(before));
2370
2708
  if (this.destroyed) return;
2709
+ this.checkLaunch();
2371
2710
  this.wireTimers();
2372
2711
  if (this.user?.id) await settlesWithin(this.identify(this.user.id, this.user.traits, this.user.account), CACHED_START_MS);
2373
2712
  if (this.destroyed) return;
@@ -2397,6 +2736,7 @@ var GuidelyClient = class {
2397
2736
  if (res.etag) this.store.set(KEYS.etag(this.options.projectId), res.etag);
2398
2737
  else this.store.remove(KEYS.etag(this.options.projectId));
2399
2738
  this.syncMessenger();
2739
+ if (this.hydrated) this.checkLaunch();
2400
2740
  this.emitter.emit("config", this.config);
2401
2741
  this.log("config loaded", this.config.configVersion, `${this.config.guides.length} guides`);
2402
2742
  return true;
@@ -2417,7 +2757,7 @@ var GuidelyClient = class {
2417
2757
  if (!this.isReady || !this.config || this.backgroundedAt !== null) return;
2418
2758
  const url = this.currentUrl();
2419
2759
  const ctx = this.getContext();
2420
- for (const guide of this.config.guides) if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
2760
+ for (const guide of this.candidates()) if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
2421
2761
  this.drainQueue(url, ctx);
2422
2762
  }, 1e3);
2423
2763
  this.cleanups.push(() => clearInterval(poll));
@@ -2441,7 +2781,9 @@ var GuidelyClient = class {
2441
2781
  setAppState(state) {
2442
2782
  if (this.destroyed || !this.hydrated) return;
2443
2783
  if (state === "background") {
2784
+ const wasForeground = this.backgroundedAt === null;
2444
2785
  this.backgroundedAt = Date.now();
2786
+ if (wasForeground) this.wentToBackground();
2445
2787
  this.session.lastActiveAt = nowIso();
2446
2788
  this.persistSession();
2447
2789
  void this.queue?.flush();
@@ -2458,6 +2800,7 @@ var GuidelyClient = class {
2458
2800
  this.persistSession();
2459
2801
  this.firedDelays.clear();
2460
2802
  }
2803
+ this.cameToForeground();
2461
2804
  void this.flags.refresh();
2462
2805
  if (Date.now() - this.configFetchedAt > this.options.configRefreshMs && this.api.health.canSend()) void this.refresh();
2463
2806
  else if (this.isReady) this.evaluate("ready");
@@ -2470,6 +2813,7 @@ var GuidelyClient = class {
2470
2813
  /** Tears everything down. Create a new client to start again. */
2471
2814
  destroy() {
2472
2815
  if (this.destroyed) return;
2816
+ this.flushHeatmap();
2473
2817
  this.destroyed = true;
2474
2818
  this.active?.controller.destroy();
2475
2819
  this.active = null;
@@ -2521,6 +2865,7 @@ var GuidelyClient = class {
2521
2865
  for (const [id, s] of Object.entries(data.guideState || {})) {
2522
2866
  const local = guides[id];
2523
2867
  guides[id] = {
2868
+ ...local,
2524
2869
  impressions: Math.max(local?.impressions ?? 0, s.impressions ?? 0),
2525
2870
  completedAt: local?.completedAt ?? s.completedAt ?? null,
2526
2871
  dismissedAt: local?.dismissedAt ?? s.dismissedAt ?? null,
@@ -2542,7 +2887,10 @@ var GuidelyClient = class {
2542
2887
  checklists,
2543
2888
  surveysSubmitted: Array.from(/* @__PURE__ */ new Set([...this.state.surveysSubmitted, ...data.surveysSubmitted || []])),
2544
2889
  audienceIds: data.audienceIds || [],
2545
- firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt
2890
+ firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt,
2891
+ // Device-local only (the server doesn't hold them).
2892
+ ...this.state.throttle ? { throttle: this.state.throttle } : {},
2893
+ ...this.state.announcementsSeenAt ? { announcementsSeenAt: this.state.announcementsSeenAt } : {}
2546
2894
  };
2547
2895
  if (data.firstSeenAt && Date.now() - Date.parse(data.firstSeenAt) > 6e4) this.firstVisit = false;
2548
2896
  this.persistState();
@@ -2634,6 +2982,8 @@ var GuidelyClient = class {
2634
2982
  ...extra
2635
2983
  };
2636
2984
  if (extra.properties) event.properties = sanitizeProperties(extra.properties);
2985
+ const locale = extra.guideId ? this.guideLocales.get(extra.guideId) : void 0;
2986
+ if (locale) event.properties = { ...event.properties ?? {}, locale };
2637
2987
  this.queue.enqueue(event);
2638
2988
  this.emitter.emit("track", event);
2639
2989
  return event;
@@ -2666,11 +3016,19 @@ var GuidelyClient = class {
2666
3016
  if (!name) return;
2667
3017
  this.whenHydrated(() => {
2668
3018
  const changed = name !== this.screenName;
3019
+ if (changed) {
3020
+ this.leaveScreen();
3021
+ this.flushHeatmap();
3022
+ }
2669
3023
  this.screenName = name;
2670
3024
  this.targets.setScreen(name);
2671
3025
  if (changed) this.syncFlagAttributes();
2672
3026
  const url = this.currentUrl();
2673
- if (changed) this.enqueue("page_view", { properties: { ...properties, path: url, screen: name } });
3027
+ if (changed) {
3028
+ this.screenStartedAt = Date.now();
3029
+ this.screenLeft = this.backgroundedAt !== null;
3030
+ if (this.pageviewsOn()) this.enqueue("page_view", { properties: { ...properties, path: url, screen: name, platform: this.bridge.device.platform } });
3031
+ }
2674
3032
  this.completeChecklistTasksBy("url_visited", url);
2675
3033
  this.active?.controller.resume();
2676
3034
  this.flows.forEach((f) => f.controller.resume());
@@ -2698,6 +3056,149 @@ var GuidelyClient = class {
2698
3056
  if (this.isReady) this.evaluate("click", { targetId });
2699
3057
  }
2700
3058
  // ---------------------------------------------------------------------------
3059
+ // Product analytics (autocapture)
3060
+ // ---------------------------------------------------------------------------
3061
+ captureAllowed() {
3062
+ return !this.destroyed && this.analyticsEnabled();
3063
+ }
3064
+ /** A capability runs when the project enables it (absent on old servers → off), no option turns it off and analytics are allowed. */
3065
+ autocaptureOn(key) {
3066
+ const o = this.options.autocapture;
3067
+ return !!this.config?.settings?.autocapture?.[key] && o !== false && !(typeof o === "object" && o?.[key] === false) && this.captureAllowed();
3068
+ }
3069
+ /** `page_view` from `screen()` predates autocapture: only an explicit opt-out stops it. */
3070
+ pageviewsOn() {
3071
+ const o = this.options.autocapture;
3072
+ return this.config?.settings?.autocapture?.pageviews !== false && o !== false && !(typeof o === "object" && o?.pageviews === false);
3073
+ }
3074
+ /** True when taps are worth observing (clicks, rage taps or heatmaps are on); the provider skips resolving views otherwise. */
3075
+ isTapCaptureOn() {
3076
+ return this.hydrated && (this.autocaptureOn("clicks") || this.autocaptureOn("rageClicks") || this.autocaptureOn("heatmaps"));
3077
+ }
3078
+ /**
3079
+ * Records a tap observed by the provider: `click` for an interactive element, `rage_click` for
3080
+ * bursts and a heatmap point. A registered `<GuidelyTarget>` whose frame contains the point
3081
+ * (smallest wins) takes precedence over the resolved element and is reported as `#<id>`.
3082
+ */
3083
+ recordTap(tap) {
3084
+ try {
3085
+ if (!this.hydrated || this.destroyed || !Number.isFinite(tap.x) || !Number.isFinite(tap.y)) return;
3086
+ const on = { clicks: this.autocaptureOn("clicks"), rage: this.autocaptureOn("rageClicks"), heat: this.autocaptureOn("heatmaps") };
3087
+ if (!on.clicks && !on.rage && !on.heat) return;
3088
+ const t = tap.t ?? Date.now();
3089
+ const url = this.screenName ? this.currentUrl() : "";
3090
+ this.tapChain = this.tapChain.then(() => this.processTap(tap, t, url, on)).catch((err) => this.reportError(err));
3091
+ } catch (err) {
3092
+ this.reportError(err);
3093
+ }
3094
+ }
3095
+ async processTap(tap, t, url, on) {
3096
+ let el = tap.element ?? null;
3097
+ let rect = null;
3098
+ const target = this.targets.ids().length ? await this.targets.hitTest(tap.x, tap.y) : null;
3099
+ if (target) {
3100
+ el = { selector: `#${target.id}`, tag: el?.tag ?? tap.hit?.tag ?? "View", text: el?.text };
3101
+ rect = target.rect;
3102
+ }
3103
+ if (this.destroyed) return;
3104
+ const platform = this.bridge.device.platform;
3105
+ const maskAll = !!this.config?.settings?.replay?.maskAllText;
3106
+ const eventUrl = this.collectUrls() && url ? url : void 0;
3107
+ if (on.clicks && el) this.enqueue("click", { url: eventUrl, properties: { ...tapProperties(el, tap.x, tap.y, platform, maskAll) } });
3108
+ const count = this.rage.add(tap.x, tap.y, t);
3109
+ if (on.rage && count) this.enqueue("rage_click", { url: eventUrl, properties: { ...tapProperties(el ?? tap.hit ?? UNKNOWN_VIEW, tap.x, tap.y, platform, maskAll), count } });
3110
+ if (!on.heat) return;
3111
+ const burst = this.rage.burst;
3112
+ if (el && !rect && el.measure) rect = await el.measure().catch(() => null);
3113
+ if (this.destroyed) return;
3114
+ if (this.heatmap.url !== url) this.flushHeatmap();
3115
+ if (this.heatmap.add(url, heatmapPoint(tap.x, tap.y, el, rect, burst > 0), burst)) this.flushHeatmap();
3116
+ }
3117
+ /** Sends the buffered heatmap taps as one `heatmap` event attributed to the screen they happened on. */
3118
+ flushHeatmap() {
3119
+ const batch = this.heatmap.take();
3120
+ if (!batch || !this.captureAllowed()) return;
3121
+ const collect = this.collectUrls() && !!batch.url;
3122
+ const { width, height } = this.windowSize();
3123
+ this.enqueue("heatmap", {
3124
+ url: collect ? batch.url : void 0,
3125
+ properties: { ...collect ? { path: batch.url } : {}, viewportWidth: width, viewportHeight: height, pageHeight: height, platform: this.bridge.device.platform, points: batch.points }
3126
+ });
3127
+ }
3128
+ windowSize() {
3129
+ try {
3130
+ const s = this.bridge.windowSize?.();
3131
+ if (s && Number.isFinite(s.width) && Number.isFinite(s.height)) return { width: Math.round(s.width), height: Math.round(s.height) };
3132
+ } catch {
3133
+ }
3134
+ return { width: 0, height: 0 };
3135
+ }
3136
+ /** Ends the foreground measurement of the current screen with a `page_leave` (once per measurement). */
3137
+ leaveScreen() {
3138
+ if (!this.screenName || this.screenLeft) return;
3139
+ this.screenLeft = true;
3140
+ if (!this.autocaptureOn("pageLeave")) return;
3141
+ const url = this.currentUrl();
3142
+ const collect = this.collectUrls();
3143
+ this.enqueue("page_leave", {
3144
+ url: collect ? url : void 0,
3145
+ properties: { ...collect ? { path: url } : {}, screen: this.screenName, durationMs: Math.max(0, Date.now() - this.screenStartedAt), scrollDepth: 100, platform: this.bridge.device.platform }
3146
+ });
3147
+ }
3148
+ /** `{ appVersion, build }` of this launch, without unknown keys. */
3149
+ appInfo() {
3150
+ const appVersion = this.options.appVersion ?? this.bridge.device.appVersion;
3151
+ const build = this.options.appBuild;
3152
+ return { ...appVersion ? { appVersion: String(appVersion) } : {}, ...build ? { build: String(build) } : {} };
3153
+ }
3154
+ /**
3155
+ * Once per launch, as soon as a config is known: `app_install` / `app_update` from the stored
3156
+ * version (when the app passes `appVersion` and `storage`), then `app_open` with `coldStart: true`.
3157
+ */
3158
+ checkLaunch() {
3159
+ if (this.launchChecked || !this.config || this.destroyed || !this.hydrated) return;
3160
+ this.launchChecked = true;
3161
+ try {
3162
+ const on = this.autocaptureOn("appLifecycle");
3163
+ const info = this.appInfo();
3164
+ if (info.appVersion && this.options.storage) {
3165
+ const key = appVersionKey(this.options.projectId);
3166
+ const stored = readJson(this.store, key, null);
3167
+ if (!stored?.appVersion) {
3168
+ if (!this.hadIdentity && on) this.enqueue("app_install", { properties: { ...info } });
3169
+ } else if (stored.appVersion !== info.appVersion || !!stored.build && !!info.build && stored.build !== info.build) {
3170
+ if (on) this.enqueue("app_update", { properties: { ...info, previousVersion: stored.appVersion, ...stored.build ? { previousBuild: stored.build } : {} } });
3171
+ }
3172
+ writeJson(this.store, key, info);
3173
+ }
3174
+ if (on) this.enqueue("app_open", { properties: { ...info, coldStart: true } });
3175
+ } catch (err) {
3176
+ this.reportError(err);
3177
+ }
3178
+ }
3179
+ wentToBackground() {
3180
+ try {
3181
+ this.leaveScreen();
3182
+ this.flushHeatmap();
3183
+ if (this.launchChecked && this.autocaptureOn("appLifecycle")) this.enqueue("app_background", { properties: { foregroundMs: Math.max(0, Date.now() - this.openedAt) } });
3184
+ } catch (err) {
3185
+ this.reportError(err);
3186
+ }
3187
+ }
3188
+ cameToForeground() {
3189
+ try {
3190
+ this.openedAt = Date.now();
3191
+ if (this.launchChecked && this.autocaptureOn("appLifecycle")) this.enqueue("app_open", { properties: { ...this.appInfo(), coldStart: false } });
3192
+ if (this.screenName && this.screenLeft) {
3193
+ this.screenStartedAt = Date.now();
3194
+ this.screenLeft = false;
3195
+ if (this.pageviewsOn() && this.autocaptureOn("pageLeave")) this.enqueue("page_view", { properties: { path: this.currentUrl(), screen: this.screenName, platform: this.bridge.device.platform, resumed: true } });
3196
+ }
3197
+ } catch (err) {
3198
+ this.reportError(err);
3199
+ }
3200
+ }
3201
+ // ---------------------------------------------------------------------------
2701
3202
  // Targeting + triggers
2702
3203
  // ---------------------------------------------------------------------------
2703
3204
  /** Builds the targeting context for the current moment. */
@@ -2744,6 +3245,50 @@ var GuidelyClient = class {
2744
3245
  if (guide.kind === "checklist") return !this.state.checklists[guide.id]?.dismissedAt;
2745
3246
  return isFrequencyEligible(guide.config.frequency, this.state.guides[guide.id], this.session.shownGuides.includes(guide.id));
2746
3247
  }
3248
+ /** Guide throttling: may this guide launch automatically now? */
3249
+ throttleAllows(guide) {
3250
+ if (!countsTowardThrottle(guide)) return true;
3251
+ return isThrottleEligible(this.config?.settings.throttling, this.state.throttle ?? []);
3252
+ }
3253
+ recordLaunch(guide, manual) {
3254
+ if (manual || !countsTowardThrottle(guide) || !this.config?.settings.throttling?.enabled) return;
3255
+ this.state.throttle = pruneLaunches([...this.state.throttle ?? [], nowIso()], this.config.settings.throttling);
3256
+ }
3257
+ /** Guides and A/B test control groups, which are evaluated the same way but never render. */
3258
+ candidates() {
3259
+ const guides = this.config?.guides ?? [];
3260
+ const holdouts = this.config?.guideHoldouts ?? [];
3261
+ return holdouts.length ? [...guides, ...holdouts.map((h) => h.guide)] : guides;
3262
+ }
3263
+ holdoutFor(guide) {
3264
+ return this.config?.guideHoldouts?.find((h) => h.guide.id === guide.id) ?? null;
3265
+ }
3266
+ // ---------------------------------------------------------------------------
3267
+ // Localization
3268
+ // ---------------------------------------------------------------------------
3269
+ /** The visitor's language candidates, most preferred first: `setLocale()` / `locale` option, traits, device. */
3270
+ locales() {
3271
+ return visitorLocales(this.locale, this.user?.traits, [this.bridge.device.locale]);
3272
+ }
3273
+ defaultLocale() {
3274
+ return this.config?.settings?.localization?.defaultLocale || "en";
3275
+ }
3276
+ /** The guide in the visitor's language (remembered so its events carry `locale`). */
3277
+ localize(guide) {
3278
+ const localized = localizeGuide(guide, this.locales(), this.defaultLocale());
3279
+ this.guideLocales.set(guide.id, localized.locale);
3280
+ return localized;
3281
+ }
3282
+ /**
3283
+ * Sets the language for guides and the resource center (`null` = back to the user's traits and
3284
+ * the device language). Guides already on screen keep their language; the resource center re-renders.
3285
+ */
3286
+ setLocale(locale) {
3287
+ const next = locale ? normalizeLocale(locale) || null : null;
3288
+ if (next === this.locale) return;
3289
+ this.locale = next;
3290
+ this.notify();
3291
+ }
2747
3292
  triggerFires(guide, reason, detail) {
2748
3293
  const t = guide.config.trigger;
2749
3294
  switch (t.type) {
@@ -2779,7 +3324,7 @@ var GuidelyClient = class {
2779
3324
  this.lastAudiences = current;
2780
3325
  }
2781
3326
  const candidates = [];
2782
- for (const guide of this.config.guides) {
3327
+ for (const guide of this.candidates()) {
2783
3328
  const t = guide.config.trigger;
2784
3329
  if (detail.endedGuide && detail.endedGuide.id === guide.id) continue;
2785
3330
  if (reason === "ready" || reason === "navigation") {
@@ -2795,6 +3340,10 @@ var GuidelyClient = class {
2795
3340
  if (t.type === "page_load" && t.delayMs > 0 && reason === "timer" && detail.guideId !== guide.id) continue;
2796
3341
  if (!this.triggerFires(guide, reason, detail)) continue;
2797
3342
  if (!this.isEligible(guide, ctx, url)) continue;
3343
+ if (!this.throttleAllows(guide)) {
3344
+ this.log("throttled", guide.key);
3345
+ continue;
3346
+ }
2798
3347
  candidates.push(guide);
2799
3348
  }
2800
3349
  if (reason === "after_guide" || reason === "navigation") this.drainQueue(url, ctx);
@@ -2840,6 +3389,12 @@ var GuidelyClient = class {
2840
3389
  }
2841
3390
  tryShow(guide, url, manual) {
2842
3391
  if (this.destroyed) return false;
3392
+ if (!manual && !this.throttleAllows(guide)) return false;
3393
+ if (this.holdoutFor(guide)) {
3394
+ if (this.hasExclusive() && !manual) return false;
3395
+ this.recordImpression(guide, manual);
3396
+ return true;
3397
+ }
2843
3398
  if (!isExclusiveGuide(guide)) {
2844
3399
  if (this.flows.has(guide.id) || this.checklists.has(guide.id)) return false;
2845
3400
  this.show(guide, manual);
@@ -2881,14 +3436,18 @@ var GuidelyClient = class {
2881
3436
  guideState(id) {
2882
3437
  return this.state.guides[id] ?? (this.state.guides[id] = { impressions: 0 });
2883
3438
  }
2884
- recordImpression(guide) {
3439
+ recordImpression(guide, manual = false) {
2885
3440
  const s = this.guideState(guide.id);
2886
3441
  s.impressions += 1;
2887
3442
  s.lastShownAt = nowIso();
2888
3443
  if (!this.session.shownGuides.includes(guide.id)) this.session.shownGuides.push(guide.id);
3444
+ this.recordLaunch(guide, manual);
2889
3445
  this.persistState();
2890
3446
  this.persistSession();
2891
- this.syncGuideState(guide.id, { impression: true });
3447
+ const holdout = this.holdoutFor(guide);
3448
+ const experiment = holdout ? { id: holdout.experimentId, variantId: holdout.variantId } : guide.experiment;
3449
+ if (experiment) this.enqueue("experiment_viewed", { properties: { guideExperimentId: experiment.id, variantId: experiment.variantId, ...holdout ? {} : { guideId: guide.id } } });
3450
+ if (!holdout) this.syncGuideState(guide.id, { impression: true });
2892
3451
  }
2893
3452
  recordEnd(guide, reason) {
2894
3453
  const s = this.guideState(guide.id);
@@ -2916,7 +3475,8 @@ var GuidelyClient = class {
2916
3475
  this.reportError(err);
2917
3476
  }
2918
3477
  }
2919
- showFlow(guide, manual, stepId) {
3478
+ showFlow(raw, manual, stepId) {
3479
+ const guide = this.localize(raw);
2920
3480
  const exclusive = isExclusiveGuide(guide);
2921
3481
  const flow = { guide, manual, view: null, controller: null };
2922
3482
  let renders = 0;
@@ -2931,7 +3491,7 @@ var GuidelyClient = class {
2931
3491
  const step = e.stepId ? guide.config.steps.find((s) => s.id === e.stepId) : void 0;
2932
3492
  this.enqueue(e.name, { guideId: guide.id, guideVersion: guide.version, stepId: e.stepId, stepIndex: e.stepIndex, properties: e.properties });
2933
3493
  if (e.name === "guide_started") {
2934
- this.recordImpression(guide);
3494
+ this.recordImpression(guide, manual);
2935
3495
  this.emitter.emit("guide:start", guide);
2936
3496
  } else if (e.name === "step_viewed" && step) {
2937
3497
  this.emitter.emit("guide:step", { guide, stepId: step.id, stepIndex: e.stepIndex ?? 0 });
@@ -2978,21 +3538,24 @@ var GuidelyClient = class {
2978
3538
  if (this.flows.get(guide.id) === flow) this.flows.delete(guide.id);
2979
3539
  }
2980
3540
  }
2981
- showChecklist(guide, open) {
3541
+ showChecklist(raw, open) {
3542
+ if (!raw.config.checklist || this.checklists.has(raw.id)) return;
3543
+ const guide = this.localize(raw);
2982
3544
  const config = guide.config.checklist;
2983
- if (!config || this.checklists.has(guide.id)) return;
3545
+ if (!config) return;
2984
3546
  const progress = this.getChecklistProgress(guide.id);
2985
3547
  const first = !this.state.guides[guide.id]?.impressions;
2986
- this.checklists.set(guide.id, { open: open ?? (config.autoOpen && first && !progress.completedAt) });
3548
+ this.checklists.set(guide.id, { guide, open: open ?? (config.autoOpen && first && !progress.completedAt) });
2987
3549
  if (first) this.recordImpression(guide);
2988
3550
  if (this.checklists.get(guide.id)?.open) this.enqueue("guide_viewed", { guideId: guide.id, guideVersion: guide.version });
2989
3551
  this.notify();
2990
3552
  }
2991
- showSurvey(guide, manual) {
2992
- if (!guide.config.survey) return;
3553
+ showSurvey(raw, manual) {
3554
+ if (!raw.config.survey) return;
3555
+ const guide = this.localize(raw);
2993
3556
  this.activeSurvey = { guide, manual };
2994
3557
  this.surveyView = { key: `${guide.id}:${randomId(6)}`, guide, submitted: false };
2995
- this.recordImpression(guide);
3558
+ this.recordImpression(guide, manual);
2996
3559
  this.enqueue("survey_displayed", { guideId: guide.id, guideVersion: guide.version });
2997
3560
  this.emitter.emit("guide:start", guide);
2998
3561
  this.notify();
@@ -3052,10 +3615,7 @@ var GuidelyClient = class {
3052
3615
  const steps = [];
3053
3616
  for (const f of [...this.flows.values(), ...this.active ? [this.active] : []]) if (f.view) steps.push({ ...f.view, expanded: this.expanded.has(f.guide.id) });
3054
3617
  const checklists = [];
3055
- this.checklists.forEach((w, id) => {
3056
- const guide = this.findGuide(id);
3057
- if (guide) checklists.push({ guide, progress: this.getChecklistProgress(id), open: w.open });
3058
- });
3618
+ this.checklists.forEach((w, id) => checklists.push({ guide: w.guide, progress: this.getChecklistProgress(id), open: w.open }));
3059
3619
  this.ui = { steps, checklists, survey: this.surveyView, resourceCenterOpen: this.resourceCenterOpen, theme: this.themeFor(), messenger: !!this.messenger };
3060
3620
  for (const cb of Array.from(this.uiListeners)) {
3061
3621
  try {
@@ -3140,10 +3700,58 @@ var GuidelyClient = class {
3140
3700
  getAudienceIds() {
3141
3701
  return this.state.audienceIds;
3142
3702
  }
3703
+ /** The published resource center in the visitor's language, or null when it is off. */
3143
3704
  getResourceCenter() {
3144
- const rc = this.config?.resourceCenter;
3705
+ const rc = this.localizedResourceCenter();
3145
3706
  return rc && rc.enabled ? rc : null;
3146
3707
  }
3708
+ /** The resource center in the visitor's language (cached until the config or language changes). */
3709
+ localizedResourceCenter() {
3710
+ const rc = this.config?.resourceCenter;
3711
+ if (!rc) return null;
3712
+ const locales = this.locales();
3713
+ const key = `${locales.join(",")}|${this.defaultLocale()}`;
3714
+ if (this.localizedRc?.source !== rc || this.localizedRc.locales !== key) this.localizedRc = { source: rc, locales: key, value: localizeResourceCenter(rc, locales, this.defaultLocale()) };
3715
+ return this.localizedRc.value;
3716
+ }
3717
+ /** Evaluates targeting rules (e.g. a resource-center section's) for the current visitor and screen. */
3718
+ matchesTargeting(rules) {
3719
+ if (!rules) return true;
3720
+ try {
3721
+ return evaluateRules(rules, this.getContext());
3722
+ } catch (err) {
3723
+ this.reportError(err);
3724
+ return false;
3725
+ }
3726
+ }
3727
+ /** Resource-center sections this visitor sees, in order (disabled and non-matching ones left out). */
3728
+ getResourceCenterModules() {
3729
+ const rc = this.getResourceCenter();
3730
+ if (!rc) return [];
3731
+ return orderedModules(rc.modules).filter((m) => m.enabled && (!("targeting" in m) || this.matchesTargeting(m.targeting))).map((m) => m.id);
3732
+ }
3733
+ /**
3734
+ * Sections of the messenger's Help tab, in order: the resource center's modules without "What's
3735
+ * new" (the messenger has its own News tab); every section when no resource center is published.
3736
+ */
3737
+ getHelpModules() {
3738
+ const modules = this.getResourceCenter() ? this.getResourceCenterModules() : [...RC_MODULE_IDS];
3739
+ return modules.filter((id) => id !== "announcements");
3740
+ }
3741
+ /** "What's new" entries this visitor sees (newest first); empty when the section is off. */
3742
+ getAnnouncements() {
3743
+ const rc = this.getResourceCenter();
3744
+ if (!rc || !this.getResourceCenterModules().includes("announcements")) return [];
3745
+ return visibleAnnouncements(rc.announcements, this.state.audienceIds);
3746
+ }
3747
+ /** "What's new" entries published since the visitor last opened the resource center. */
3748
+ getUnreadAnnouncements() {
3749
+ return unreadAnnouncements(this.getAnnouncements(), this.state.announcementsSeenAt);
3750
+ }
3751
+ /** Whether an entry is new to the visitor in the resource center that is open now. */
3752
+ isAnnouncementNew(a) {
3753
+ return unreadAnnouncements([a], this.announcementsSeenAtOpen) > 0;
3754
+ }
3147
3755
  async searchResourceCenter(q) {
3148
3756
  try {
3149
3757
  const res = await this.api.get("/resource-center/search", { params: { q } });
@@ -3152,6 +3760,11 @@ var GuidelyClient = class {
3152
3760
  return [];
3153
3761
  }
3154
3762
  }
3763
+ /** The language of the resource center as shown, when it isn't the default (sent with article fetches). */
3764
+ articleLocale() {
3765
+ const locale = this.localizedResourceCenter()?.locale;
3766
+ return locale && locale.toLowerCase() !== this.defaultLocale().toLowerCase() ? locale : void 0;
3767
+ }
3155
3768
  articleViewed(article) {
3156
3769
  this.enqueue("article_viewed", { properties: { articleId: article.id, slug: article.slug } });
3157
3770
  }
@@ -3286,6 +3899,11 @@ var GuidelyClient = class {
3286
3899
  }
3287
3900
  if (this.resourceCenterOpen) return;
3288
3901
  this.resourceCenterOpen = true;
3902
+ this.announcementsSeenAtOpen = this.state.announcementsSeenAt ?? null;
3903
+ if (this.getAnnouncements().length) {
3904
+ this.state.announcementsSeenAt = nowIso();
3905
+ this.persistState();
3906
+ }
3289
3907
  this.enqueue("resource_center_opened");
3290
3908
  this.emitter.emit("resource-center:open");
3291
3909
  this.notify();
@@ -3321,7 +3939,8 @@ var GuidelyClient = class {
3321
3939
  onMessage: (message, conversation) => this.emitter.emit("conversation:message", { message, conversation }),
3322
3940
  onStartGuide: (key) => this.setTimer(() => this.startGuide(key), 0),
3323
3941
  onError: (err) => this.log("messenger", err instanceof Error ? err.message : err),
3324
- health: this.api.health
3942
+ health: this.api.health,
3943
+ locale: () => this.articleLocale()
3325
3944
  });
3326
3945
  this.resourceCenterOpen = false;
3327
3946
  }
@@ -3455,7 +4074,191 @@ var GuidelyClient = class {
3455
4074
  }
3456
4075
  };
3457
4076
  function forPlatform(config, platform) {
3458
- return config.guides.every((g) => supportsPlatform(g.config, platform)) ? config : { ...config, guides: config.guides.filter((g) => supportsPlatform(g.config, platform)) };
4077
+ const keep = (g) => supportsPlatform(g.config, platform);
4078
+ const holdouts = config.guideHoldouts?.filter((h) => keep(h.guide)).map((h) => ({ ...h, guide: withoutCode(h.guide) }));
4079
+ return { ...config, guides: config.guides.filter(keep).map(withoutCode), ...holdouts ? { guideHoldouts: holdouts } : {} };
4080
+ }
4081
+ function withoutCode(guide) {
4082
+ if (!guide.config.steps.some((s) => s.code)) return guide;
4083
+ return { ...guide, config: { ...guide.config, steps: guide.config.steps.map(({ code: _code, ...step }) => step) } };
4084
+ }
4085
+
4086
+ // src/ui/Autocapture.tsx
4087
+ var import_react8 = require("react");
4088
+ var import_react_native11 = require("react-native");
4089
+
4090
+ // src/core/viewTree.ts
4091
+ var HOST_TEXT = 6;
4092
+ var MAX_ANCESTORS = 500;
4093
+ var MAX_TEXT_NODES = 300;
4094
+ var INTERACTIVE_ROLES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem", "checkbox", "switch", "radio", "togglebutton", "imagebutton", "option", "combobox", "adjustable"]);
4095
+ var TEXT_INPUT_HOST = /TextInput/i;
4096
+ var NOISE = /^(Anonymous|Unknown|ForwardRef|Memo|Context|Provider|Consumer|Fragment|StrictMode|Suspense|Profiler)$|Context$|Provider$|Consumer$/;
4097
+ var isFiber = (v) => !!v && typeof v === "object" && "return" in v && "memoizedProps" in v;
4098
+ function fiberFromEvent(event) {
4099
+ try {
4100
+ const e = event;
4101
+ if (isFiber(e?._targetInst)) return e._targetInst;
4102
+ const target = e?.target;
4103
+ if (!target || typeof target !== "object") return null;
4104
+ for (const key of ["__internalInstanceHandle", "_internalInstanceHandle", "_internalFiberInstanceHandleDEV"]) if (isFiber(target[key])) return target[key];
4105
+ for (const sym of Object.getOwnPropertySymbols(target)) if (sym.description === "internalInstanceHandle" && isFiber(target[sym])) return target[sym];
4106
+ } catch {
4107
+ }
4108
+ return null;
4109
+ }
4110
+ function componentName(type, depth = 0) {
4111
+ if (!type || depth > 3) return null;
4112
+ if (typeof type === "function") {
4113
+ const f = type;
4114
+ const n = typeof f.displayName === "string" && f.displayName ? f.displayName : typeof f.name === "string" ? f.name : "";
4115
+ return n || null;
4116
+ }
4117
+ if (typeof type === "object") {
4118
+ const o = type;
4119
+ if (typeof o.displayName === "string" && o.displayName) return o.displayName;
4120
+ return componentName(o.render, depth + 1) ?? componentName(o.type, depth + 1);
4121
+ }
4122
+ return null;
4123
+ }
4124
+ var isHost = (f) => typeof f.type === "string";
4125
+ var isTextInput = (f) => typeof f.type === "string" ? TEXT_INPUT_HOST.test(f.type) : componentName(f.type) === "TextInput";
4126
+ function isInteractive(props) {
4127
+ if (!props || typeof props !== "object") return null;
4128
+ if (typeof props.onPress === "function" || typeof props.onLongPress === "function") return "press";
4129
+ const role = props.accessibilityRole ?? props.role;
4130
+ return typeof role === "string" && INTERACTIVE_ROLES.has(role) ? "role" : null;
4131
+ }
4132
+ function firstHost(f) {
4133
+ let cur = f;
4134
+ for (let i = 0; cur && i < 30; i++, cur = cur.child) if (isHost(cur)) return cur;
4135
+ return null;
4136
+ }
4137
+ function idSelector(f) {
4138
+ const host = firstHost(f);
4139
+ const sources = [f.memoizedProps, host?.memoizedProps];
4140
+ for (const key of ["nativeID", "testID"]) {
4141
+ for (const p of sources) {
4142
+ const v = p && typeof p === "object" ? p[key] : void 0;
4143
+ if (typeof v === "string" && v) return attrSelector(key, v);
4144
+ }
4145
+ }
4146
+ return null;
4147
+ }
4148
+ function pathSelector(f) {
4149
+ const parts = [];
4150
+ let cur = f;
4151
+ for (let i = 0; cur && parts.length < 4 && i < MAX_ANCESTORS; i++, cur = cur.return) {
4152
+ if (isHost(cur)) continue;
4153
+ const name = componentName(cur.type);
4154
+ if (!name || NOISE.test(name)) continue;
4155
+ let same = 0;
4156
+ let index = 0;
4157
+ for (let s = cur.return?.child, n = 0; s && n < 500; s = s.sibling, n++) {
4158
+ if (s.type !== cur.type) continue;
4159
+ same += 1;
4160
+ if (s === cur) index = same;
4161
+ }
4162
+ parts.unshift(same > 1 && index ? `${name}:nth-of-type(${index})` : name);
4163
+ }
4164
+ return parts.join(" > ").slice(0, 300) || "View";
4165
+ }
4166
+ function subtreeText(f, opts) {
4167
+ const out = [];
4168
+ let visited = 0;
4169
+ let masked = false;
4170
+ const walk = (node) => {
4171
+ for (let n = node; n && visited < MAX_TEXT_NODES && !masked; n = n.sibling) {
4172
+ visited += 1;
4173
+ if (opts.isMask?.(n.type)) {
4174
+ masked = true;
4175
+ return;
4176
+ }
4177
+ if (isTextInput(n)) continue;
4178
+ if (n.tag === HOST_TEXT && (typeof n.memoizedProps === "string" || typeof n.memoizedProps === "number")) out.push(String(n.memoizedProps));
4179
+ else walk(n.child);
4180
+ }
4181
+ };
4182
+ if (opts.isMask?.(f.type)) return { text: "", masked: true };
4183
+ walk(f.child);
4184
+ return { text: out.join(" "), masked };
4185
+ }
4186
+ function measureHost(host) {
4187
+ return new Promise((resolve) => {
4188
+ let settled = false;
4189
+ const done = (r) => {
4190
+ if (settled) return;
4191
+ settled = true;
4192
+ clearTimeout(timer);
4193
+ resolve(r);
4194
+ };
4195
+ const timer = setTimeout(() => done(null), 500);
4196
+ const cb = (x, y, width, height) => done(Number.isFinite(x) && Number.isFinite(y) && width > 0 && height > 0 ? { x, y, width, height } : null);
4197
+ try {
4198
+ const sn = host.stateNode;
4199
+ const pub = sn?.canonical?.publicInstance ?? sn;
4200
+ if (pub && typeof pub.measureInWindow === "function") return pub.measureInWindow(cb);
4201
+ const ui = globalThis.nativeFabricUIManager;
4202
+ if (sn?.node && typeof ui?.measureInWindow === "function") return ui.measureInWindow(sn.node, cb);
4203
+ } catch {
4204
+ }
4205
+ done(null);
4206
+ });
4207
+ }
4208
+ function describe(f, masked, opts, withText) {
4209
+ const props = f.memoizedProps && typeof f.memoizedProps === "object" ? f.memoizedProps : {};
4210
+ const host = firstHost(f);
4211
+ const el = { selector: idSelector(f) ?? pathSelector(f), tag: componentName(f.type) ?? "View" };
4212
+ if (withText && !masked) {
4213
+ const sub = subtreeText(f, opts);
4214
+ if (!sub.masked) {
4215
+ const hostProps = host?.memoizedProps && typeof host.memoizedProps === "object" ? host.memoizedProps : {};
4216
+ const label = props.accessibilityLabel ?? props["aria-label"] ?? hostProps.accessibilityLabel ?? hostProps["aria-label"];
4217
+ const text = cleanText(sub.text) ?? (typeof label === "string" ? cleanText(label) : void 0);
4218
+ if (text) el.text = text;
4219
+ }
4220
+ }
4221
+ if (host) el.measure = () => measureHost(host);
4222
+ return el;
4223
+ }
4224
+ function resolveFiber(target, opts = {}) {
4225
+ const none = { ignored: false, element: null, hit: null };
4226
+ if (!target) return none;
4227
+ try {
4228
+ let press = null;
4229
+ let role = null;
4230
+ let hitFiber = null;
4231
+ let stopped = false;
4232
+ let masked = false;
4233
+ for (let f = target, i = 0; f && i < MAX_ANCESTORS; f = f.return, i++) {
4234
+ if (opts.isIgnored?.(f.type)) return { ignored: true, element: null, hit: null };
4235
+ if (opts.isMask?.(f.type)) masked = true;
4236
+ if (stopped) continue;
4237
+ if (isTextInput(f)) {
4238
+ stopped = true;
4239
+ continue;
4240
+ }
4241
+ if (isHost(f)) continue;
4242
+ const name = componentName(f.type);
4243
+ if (!hitFiber && name && !NOISE.test(name)) hitFiber = f;
4244
+ const kind = isInteractive(f.memoizedProps);
4245
+ if (kind === "press") {
4246
+ press = f;
4247
+ stopped = true;
4248
+ } else if (kind === "role" && !role) role = f;
4249
+ }
4250
+ const element = press ?? role;
4251
+ return {
4252
+ ignored: false,
4253
+ element: element ? describe(element, masked, opts, true) : null,
4254
+ hit: hitFiber ? describe(hitFiber, masked, opts, componentName(hitFiber.type) === "Text") : null
4255
+ };
4256
+ } catch {
4257
+ return none;
4258
+ }
4259
+ }
4260
+ function resolveTapTarget(event, opts = {}) {
4261
+ return resolveFiber(fiberFromEvent(event), opts);
3459
4262
  }
3460
4263
 
3461
4264
  // src/ui/Overlay.tsx
@@ -4215,25 +5018,41 @@ function Help({ store, client, palette: p }) {
4215
5018
  ] }, title);
4216
5019
  const audiences = client.getAudienceIds();
4217
5020
  const sections = resourceCenterSections(articles?.collections ?? [], articles?.articles ?? [], audiences);
4218
- const listed = results?.filter((a) => isArticleListed(articles?.collections ?? [], a, audiences));
5021
+ const modules = client.getHelpModules();
5022
+ const searchOn = modules.includes("search");
5023
+ const listed = searchOn ? results?.filter((a) => isArticleListed(articles?.collections ?? [], a, audiences)) : void 0;
5024
+ const checklists = (rc?.checklistGuideIds ?? []).map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g && g.kind === "checklist");
5025
+ const renderModule = (id) => {
5026
+ switch (id) {
5027
+ case "guides":
5028
+ return recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u25B6"))) : null;
5029
+ case "checklists":
5030
+ return checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u2611"))) : null;
5031
+ case "articles":
5032
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
5033
+ !articles ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.ActivityIndicator, { color: p.muted }) : null,
5034
+ sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => open(a)))))
5035
+ ] });
5036
+ case "faqs":
5037
+ return rc?.faqs.length ? section(
5038
+ "FAQ",
5039
+ rc.faqs.map((f) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.View, { style: [styles4.row, styles4.faq, { borderColor: p.border }], children: [
5040
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.Pressable, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles4.faqHead, children: [
5041
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.rowTitle, styles4.flex, { color: p.text }], children: f.question }),
5042
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
5043
+ ] }),
5044
+ faq === f.id ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
5045
+ ] }, f.id))
5046
+ ) : null;
5047
+ case "links":
5048
+ return rc?.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null;
5049
+ default:
5050
+ return null;
5051
+ }
5052
+ };
4219
5053
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.ScrollView, { contentContainerStyle: styles4.content, keyboardShouldPersistTaps: "handled", children: [
4220
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.TextInput, { value: q, onChangeText: setQ, placeholder: "Search articles", placeholderTextColor: p.muted, accessibilityLabel: "Search help articles", returnKeyType: "search", style: [styles4.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }] }),
4221
- listed ? section(`Results for \u201C${q.trim()}\u201D`, listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => open(a))) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.rowSub, { color: p.muted }], children: "No articles found." })) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
4222
- recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u25B6"))) : null,
4223
- !articles ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.ActivityIndicator, { color: p.muted }) : null,
4224
- sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => open(a))))),
4225
- rc?.faqs.length ? section(
4226
- "FAQ",
4227
- rc.faqs.map((f) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.View, { style: [styles4.row, styles4.faq, { borderColor: p.border }], children: [
4228
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.Pressable, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles4.faqHead, children: [
4229
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.rowTitle, styles4.flex, { color: p.text }], children: f.question }),
4230
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
4231
- ] }),
4232
- faq === f.id ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
4233
- ] }, f.id))
4234
- ) : null,
4235
- rc?.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null
4236
- ] })
5054
+ searchOn ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.TextInput, { value: q, onChangeText: setQ, placeholder: "Search articles", placeholderTextColor: p.muted, accessibilityLabel: "Search help articles", returnKeyType: "search", style: [styles4.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }] }) : null,
5055
+ listed ? section(`Results for \u201C${q.trim()}\u201D`, listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => open(a))) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.rowSub, { color: p.muted }], children: "No articles found." })) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: modules.filter((id) => id !== "search").map((id) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react3.Fragment, { children: renderModule(id) }, id)) })
4237
5056
  ] });
4238
5057
  }
4239
5058
  function Article({ article, palette: p, client }) {
@@ -4336,6 +5155,8 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4336
5155
  const checklists = (0, import_react4.useMemo)(() => (rc?.checklistGuideIds ?? []).map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g && g.kind === "checklist"), [rc, guides]);
4337
5156
  if (!rc) return null;
4338
5157
  const close = () => client.closeResourceCenter();
5158
+ const modules = client.getResourceCenterModules();
5159
+ const announcements = client.getAnnouncements();
4339
5160
  const openArticle = (a) => {
4340
5161
  if (a.externalUrl) return client.navigate(a.externalUrl);
4341
5162
  client.articleViewed(a);
@@ -4354,7 +5175,51 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4354
5175
  ] }, title);
4355
5176
  const audiences = client.getAudienceIds();
4356
5177
  const sections = resourceCenterSections(rc.collections, rc.articles, audiences);
4357
- const listed = results?.filter((a) => isArticleListed(rc.collections, a, audiences));
5178
+ const listed = modules.includes("search") ? results?.filter((a) => isArticleListed(rc.collections, a, audiences)) : void 0;
5179
+ const link = (label, onPress) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress, hitSlop: 8, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.link, { color: p.primary }], children: label }) }, label);
5180
+ const announcement2 = (a) => {
5181
+ const linked = a.articleId ? rc.articles.find((x) => x.id === a.articleId) : void 0;
5182
+ const isNew = client.isAnnouncementNew(a);
5183
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
5184
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: styles5.faqHead, children: [
5185
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { accessibilityRole: "header", style: [styles5.rowTitle, styles5.flex, { color: p.text, fontFamily: p.fontFamily }], children: a.title }),
5186
+ isNew ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.badge, { color: p.onPrimary, backgroundColor: p.primary }], children: "NEW" }) : null
5187
+ ] }),
5188
+ a.body ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RichText, { html: a.body, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null,
5189
+ a.guideId || linked || a.url ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: styles5.links, children: [
5190
+ a.guideId ? link("Show me", () => (close(), client.startGuide(a.guideId))) : null,
5191
+ linked ? link("Read more", () => openArticle(linked)) : null,
5192
+ a.url ? link(a.linkLabel || "Learn more", () => client.navigate(a.url)) : null
5193
+ ] }) : null
5194
+ ] }, a.id);
5195
+ };
5196
+ const renderModule = (id) => {
5197
+ switch (id) {
5198
+ case "announcements":
5199
+ return announcements.length ? section("What's new", announcements.map(announcement2)) : null;
5200
+ case "guides":
5201
+ return recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => (close(), client.startGuide(g.id)), "\u25B6"))) : null;
5202
+ case "checklists":
5203
+ return checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => (close(), client.openChecklist(g.id)), "\u2611"))) : null;
5204
+ case "articles":
5205
+ return sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => openArticle(a)))));
5206
+ case "faqs":
5207
+ return rc.faqs.length ? section(
5208
+ "FAQ",
5209
+ rc.faqs.map((f) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
5210
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.Pressable, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles5.faqHead, children: [
5211
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.rowTitle, styles5.flex, { color: p.text }], children: f.question }),
5212
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
5213
+ ] }),
5214
+ faq === f.id ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
5215
+ ] }, f.id))
5216
+ ) : null;
5217
+ case "links":
5218
+ return rc.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null;
5219
+ default:
5220
+ return null;
5221
+ }
5222
+ };
4358
5223
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Modal, { visible: open, animationType: "slide", presentationStyle: import_react_native6.Platform.OS === "ios" ? "pageSheet" : "fullScreen", onRequestClose: () => article ? setArticle(null) : close(), children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.sheet, { backgroundColor: p.background, paddingTop: import_react_native6.Platform.OS === "ios" ? 8 : topInset }], children: [
4359
5224
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.header, { borderColor: p.border }], children: [
4360
5225
  article ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Pressable, { accessibilityRole: "button", accessibilityLabel: "Back", onPress: () => setArticle(null), hitSlop: 10, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.back, { color: p.primary }], children: "\u2039 Back" }) }) : null,
@@ -4365,7 +5230,7 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4365
5230
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { accessibilityRole: "header", style: [styles5.articleTitle, { color: p.text, fontFamily: p.fontFamily }], children: article.title }),
4366
5231
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RichText, { html: article.body, palette: p, onLink: (u) => client.navigate(u) })
4367
5232
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.ScrollView, { contentContainerStyle: [styles5.content, { paddingBottom: bottomInset + 24 }], keyboardShouldPersistTaps: "handled", children: [
4368
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5233
+ modules.includes("search") ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4369
5234
  import_react_native6.TextInput,
4370
5235
  {
4371
5236
  value: query,
@@ -4377,26 +5242,11 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4377
5242
  returnKeyType: "search",
4378
5243
  style: [styles5.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }]
4379
5244
  }
4380
- ),
5245
+ ) : null,
4381
5246
  listed ? section(
4382
5247
  `Results for \u201C${query.trim()}\u201D`,
4383
5248
  listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => openArticle(a))) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.rowSub, { color: p.muted }], children: "No articles found." })
4384
- ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
4385
- recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => (close(), client.startGuide(g.id)), "\u25B6"))) : null,
4386
- checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => (close(), client.openChecklist(g.id)), "\u2611"))) : null,
4387
- sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => openArticle(a))))),
4388
- rc.faqs.length ? section(
4389
- "FAQ",
4390
- rc.faqs.map((f) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
4391
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.Pressable, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles5.faqHead, children: [
4392
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.rowTitle, styles5.flex, { color: p.text }], children: f.question }),
4393
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
4394
- ] }),
4395
- faq === f.id ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
4396
- ] }, f.id))
4397
- ) : null,
4398
- rc.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null
4399
- ] })
5249
+ ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: modules.filter((id) => id !== "search").map((id) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react4.Fragment, { children: renderModule(id) }, id)) })
4400
5250
  ] })
4401
5251
  ] }) });
4402
5252
  }
@@ -4416,7 +5266,10 @@ var styles5 = import_react_native6.StyleSheet.create({
4416
5266
  chev: { fontSize: 18 },
4417
5267
  faq: { flexDirection: "column", alignItems: "stretch" },
4418
5268
  faqHead: { flexDirection: "row", alignItems: "center", gap: 10 },
4419
- articleTitle: { fontSize: 22, fontWeight: "700" }
5269
+ articleTitle: { fontSize: 22, fontWeight: "700" },
5270
+ badge: { fontSize: 10, fontWeight: "800", letterSpacing: 0.5, paddingHorizontal: 6, paddingVertical: 2, borderRadius: 6, overflow: "hidden" },
5271
+ links: { flexDirection: "row", flexWrap: "wrap", gap: 16, marginTop: 4 },
5272
+ link: { fontSize: 14, fontWeight: "600" }
4420
5273
  });
4421
5274
 
4422
5275
  // src/ui/StepLayer.tsx
@@ -4462,7 +5315,10 @@ function positionCard(target, size, viewport, insets, placement, offset) {
4462
5315
 
4463
5316
  // src/ui/StepCard.tsx
4464
5317
  var import_react_native7 = require("react-native");
4465
- var import_jsx_runtime7 = require("react/jsx-runtime");
5318
+ var import_jsx_runtime7 = (
5319
+ // An empty description marks the image as decorative: screen readers skip it.
5320
+ require("react/jsx-runtime")
5321
+ );
4466
5322
  function StepCard({ view, client, palette: p, width, style, compact }) {
4467
5323
  const { step, guide } = view;
4468
5324
  const buttons = visibleButtons(step, view.canGoBack);
@@ -4470,7 +5326,16 @@ function StepCard({ view, client, palette: p, width, style, compact }) {
4470
5326
  const showProgress = step.showProgress && view.total > 1;
4471
5327
  const onLink = (url) => client.navigate(url);
4472
5328
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Surface, { palette: p, style: [{ width }, style], children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { accessible: false, accessibilityViewIsModal: view.asModal || step.type === "modal", style: compact ? styles6.compact : styles6.inner, children: [
4473
- step.imageUrl ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Image, { source: { uri: step.imageUrl }, style: [styles6.image, { borderRadius: Math.max(0, p.radius - 4) }], resizeMode: "cover", accessibilityIgnoresInvertColors: true }) : null,
5329
+ step.imageUrl ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
5330
+ import_react_native7.Image,
5331
+ {
5332
+ source: { uri: step.imageUrl },
5333
+ style: [styles6.image, { borderRadius: Math.max(0, p.radius - 4) }],
5334
+ resizeMode: "cover",
5335
+ accessibilityIgnoresInvertColors: true,
5336
+ ...step.imageAlt?.trim() ? { accessible: true, accessibilityRole: "image", accessibilityLabel: step.imageAlt.trim() } : { accessible: false, importantForAccessibility: "no" }
5337
+ }
5338
+ ) : null,
4474
5339
  step.title ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: styles6.header, children: [
4475
5340
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { accessibilityRole: "header", style: [styles6.title, { color: p.text, fontFamily: p.fontFamily }], children: step.title }),
4476
5341
  step.showClose ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CloseButton, { palette: p, onPress: () => client.closeStep(guide.id), label: "Dismiss guide" }) : null
@@ -5058,18 +5923,22 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
5058
5923
  const basePalette = palette(ui.theme, scheme);
5059
5924
  const messenger = ui.messenger ? client.getMessenger() : null;
5060
5925
  const showRcLauncher = !messenger && !!client.options.showResourceCenterLauncher && !!client.getResourceCenter();
5926
+ const unread = showRcLauncher ? client.getUnreadAnnouncements() : 0;
5061
5927
  const messengerLeft = messenger?.getSnapshot().config.launcher.position === "bottom-left";
5062
5928
  const checklistOnSameSide = ui.checklists.some((c) => c.guide.config.checklist?.position === "bottom-left" === messengerLeft) && client.options.showChecklistLauncher !== false;
5063
5929
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native10.View, { ref, collapsable: false, pointerEvents: "box-none", style: import_react_native10.StyleSheet.absoluteFill, onLayout: measureOrigin, children: [
5064
5930
  ui.checklists.map((c) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Checklist, { view: c, client, palette: palette(client.themeFor(c.guide), scheme), geometry, showLauncher: client.options.showChecklistLauncher !== false, launcherBottom: launcherOffset }, c.guide.id)),
5065
- showRcLauncher ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
5931
+ showRcLauncher ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
5066
5932
  import_react_native10.Pressable,
5067
5933
  {
5068
5934
  accessibilityRole: "button",
5069
- accessibilityLabel: client.getResourceCenter()?.launcher.label || "Help",
5935
+ accessibilityLabel: rcLauncherLabel(client),
5070
5936
  onPress: () => client.openResourceCenter(),
5071
5937
  style: [styles9.rc, client.getResourceCenter()?.launcher.position === "bottom-left" ? { left: insets.left + 16 } : { right: insets.right + 16 }, { bottom: insets.bottom + launcherOffset + (ui.checklists.length ? 56 : 0), backgroundColor: basePalette.primary }],
5072
- children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native10.Text, { style: [styles9.rcText, { color: basePalette.onPrimary }], children: "?" })
5938
+ children: [
5939
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native10.Text, { style: [styles9.rcText, { color: basePalette.onPrimary }], children: "?" }),
5940
+ unread ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native10.View, { pointerEvents: "none", style: [styles9.rcBadge, { borderColor: basePalette.primary }], children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native10.Text, { style: styles9.rcBadgeText, children: unread > 9 ? "9+" : unread }) }) : null
5941
+ ]
5073
5942
  }
5074
5943
  ) : null,
5075
5944
  messenger && client.options.showMessengerLauncher !== false ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MessengerLauncher, { store: messenger, palette: basePalette, style: [messengerLeft ? { left: insets.left + 16 } : { right: insets.right + 16 }, { bottom: insets.bottom + launcherOffset + (checklistOnSameSide ? 56 : 0) }] }) : null,
@@ -5080,28 +5949,76 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
5080
5949
  }
5081
5950
  var styles9 = import_react_native10.StyleSheet.create({
5082
5951
  rc: { position: "absolute", width: 48, height: 48, borderRadius: 24, alignItems: "center", justifyContent: "center", elevation: 8, shadowColor: "#000", shadowOpacity: 0.2, shadowRadius: 10, shadowOffset: { width: 0, height: 4 } },
5083
- rcText: { fontSize: 22, fontWeight: "800" }
5952
+ rcText: { fontSize: 22, fontWeight: "800" },
5953
+ rcBadge: { position: "absolute", top: -4, right: -4, minWidth: 20, height: 20, paddingHorizontal: 5, borderRadius: 10, borderWidth: 2, backgroundColor: "#dc2626", alignItems: "center", justifyContent: "center" },
5954
+ rcBadgeText: { color: "#ffffff", fontSize: 11, fontWeight: "800" }
5084
5955
  });
5956
+ function rcLauncherLabel(client) {
5957
+ const label = client.getResourceCenter()?.launcher.label || "Help";
5958
+ const unread = client.getUnreadAnnouncements();
5959
+ return unread ? `${label} (${unread} new update${unread === 1 ? "" : "s"})` : label;
5960
+ }
5085
5961
 
5086
- // src/ui/Target.tsx
5087
- var import_react8 = require("react");
5088
- var import_react_native11 = require("react-native");
5962
+ // src/ui/Autocapture.tsx
5089
5963
  var import_jsx_runtime11 = require("react/jsx-runtime");
5090
- var GuidelyContext = (0, import_react8.createContext)(null);
5091
- var GuidelyScreenContext = (0, import_react8.createContext)(null);
5964
+ function GuidelyMask({ children, ...rest }) {
5965
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native11.View, { ...rest, children });
5966
+ }
5967
+ var RESOLVE = {
5968
+ isIgnored: (type) => type === GuidelyOverlay,
5969
+ isMask: (type) => type === GuidelyMask
5970
+ };
5971
+ function useTapObserver(client) {
5972
+ const clientRef = (0, import_react8.useRef)(client);
5973
+ clientRef.current = client;
5974
+ return (0, import_react8.useMemo)(() => {
5975
+ const tracker = new TapTracker();
5976
+ const guard = (fn) => (e) => {
5977
+ try {
5978
+ fn(e);
5979
+ } catch {
5980
+ tracker.cancel();
5981
+ }
5982
+ };
5983
+ return {
5984
+ onTouchStartCapture: guard((e) => {
5985
+ const n = e.nativeEvent;
5986
+ if (!clientRef.current?.isTapCaptureOn()) return tracker.cancel();
5987
+ tracker.begin(n.pageX, n.pageY, Date.now(), Array.isArray(n.touches) ? n.touches.length : 1, n.identifier);
5988
+ }),
5989
+ onTouchMoveCapture: guard((e) => tracker.move(e.nativeEvent.pageX, e.nativeEvent.pageY, e.nativeEvent.identifier)),
5990
+ onTouchEndCapture: guard((e) => {
5991
+ const n = e.nativeEvent;
5992
+ const tap = tracker.end(n.pageX, n.pageY, Date.now(), n.identifier);
5993
+ const c = clientRef.current;
5994
+ if (!tap || !c) return;
5995
+ const r = resolveTapTarget(e, RESOLVE);
5996
+ if (!r.ignored) c.recordTap({ x: tap.x, y: tap.y, t: Date.now(), element: r.element, hit: r.hit });
5997
+ }),
5998
+ onTouchCancelCapture: guard(() => tracker.cancel())
5999
+ };
6000
+ }, []);
6001
+ }
6002
+
6003
+ // src/ui/Target.tsx
6004
+ var import_react9 = require("react");
6005
+ var import_react_native12 = require("react-native");
6006
+ var import_jsx_runtime12 = require("react/jsx-runtime");
6007
+ var GuidelyContext = (0, import_react9.createContext)(null);
6008
+ var GuidelyScreenContext = (0, import_react9.createContext)(null);
5092
6009
  function GuidelyScreenScope({ name, children }) {
5093
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(GuidelyScreenContext.Provider, { value: name, children });
6010
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(GuidelyScreenContext.Provider, { value: name, children });
5094
6011
  }
5095
6012
  function guidelyScreenLayout({ route, children }) {
5096
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(GuidelyScreenScope, { name: route.name, children });
6013
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(GuidelyScreenScope, { name: route.name, children });
5097
6014
  }
5098
- var TAP_SLOP = 10;
6015
+ var TAP_SLOP2 = 10;
5099
6016
  function useGuidelyTarget(id) {
5100
- const client = (0, import_react8.useContext)(GuidelyContext);
5101
- const screen = (0, import_react8.useContext)(GuidelyScreenContext);
5102
- const ref = (0, import_react8.useRef)(null);
5103
- const start = (0, import_react8.useRef)(null);
5104
- (0, import_react8.useEffect)(() => {
6017
+ const client = (0, import_react9.useContext)(GuidelyContext);
6018
+ const screen = (0, import_react9.useContext)(GuidelyScreenContext);
6019
+ const ref = (0, import_react9.useRef)(null);
6020
+ const start = (0, import_react9.useRef)(null);
6021
+ (0, import_react9.useEffect)(() => {
5105
6022
  if (!client || !id) return;
5106
6023
  return client.targets.register(id, {
5107
6024
  screen,
@@ -5112,15 +6029,15 @@ function useGuidelyTarget(id) {
5112
6029
  })
5113
6030
  });
5114
6031
  }, [client, id, screen]);
5115
- const onLayout = (0, import_react8.useCallback)(() => client?.targets.changed(), [client]);
5116
- const onTouchStart = (0, import_react8.useCallback)((e) => {
6032
+ const onLayout = (0, import_react9.useCallback)(() => client?.targets.changed(), [client]);
6033
+ const onTouchStart = (0, import_react9.useCallback)((e) => {
5117
6034
  start.current = { x: e.nativeEvent.pageX, y: e.nativeEvent.pageY };
5118
6035
  }, []);
5119
- const onTouchEnd = (0, import_react8.useCallback)(
6036
+ const onTouchEnd = (0, import_react9.useCallback)(
5120
6037
  (e) => {
5121
6038
  const s = start.current;
5122
6039
  start.current = null;
5123
- if (s && Math.abs(e.nativeEvent.pageX - s.x) < TAP_SLOP && Math.abs(e.nativeEvent.pageY - s.y) < TAP_SLOP) client?.targets.tapped(id);
6040
+ if (s && Math.abs(e.nativeEvent.pageX - s.x) < TAP_SLOP2 && Math.abs(e.nativeEvent.pageY - s.y) < TAP_SLOP2) client?.targets.tapped(id);
5124
6041
  },
5125
6042
  [client, id]
5126
6043
  );
@@ -5128,8 +6045,8 @@ function useGuidelyTarget(id) {
5128
6045
  }
5129
6046
  function GuidelyTarget({ id, children, onLayout, onTouchStart, onTouchEnd, ...rest }) {
5130
6047
  const t = useGuidelyTarget(id);
5131
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
5132
- import_react_native11.View,
6048
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
6049
+ import_react_native12.View,
5133
6050
  {
5134
6051
  ...rest,
5135
6052
  ref: t.ref,
@@ -5153,7 +6070,7 @@ function GuidelyTarget({ id, children, onLayout, onTouchStart, onTouchEnd, ...re
5153
6070
 
5154
6071
  // src/index.tsx
5155
6072
  var import_flags6 = require("@guidelytech/flags");
5156
- var import_jsx_runtime12 = require("react/jsx-runtime");
6073
+ var import_jsx_runtime13 = require("react/jsx-runtime");
5157
6074
  function deviceLocale() {
5158
6075
  try {
5159
6076
  return Intl.DateTimeFormat().resolvedOptions().locale;
@@ -5162,33 +6079,34 @@ function deviceLocale() {
5162
6079
  }
5163
6080
  }
5164
6081
  function defaultBridge(appVersion) {
5165
- const { width, height } = import_react_native12.Dimensions.get("window");
6082
+ const { width, height } = import_react_native13.Dimensions.get("window");
5166
6083
  return {
5167
6084
  device: {
5168
- platform: import_react_native12.Platform.OS === "android" ? "android" : "ios",
5169
- deviceType: import_react_native12.Platform.OS === "ios" && import_react_native12.Platform.isPad || Math.min(width, height) >= 600 ? "tablet" : "mobile",
6085
+ platform: import_react_native13.Platform.OS === "android" ? "android" : "ios",
6086
+ deviceType: import_react_native13.Platform.OS === "ios" && import_react_native13.Platform.isPad || Math.min(width, height) >= 600 ? "tablet" : "mobile",
5170
6087
  locale: deviceLocale(),
5171
6088
  appVersion
5172
6089
  },
5173
- openExternal: (url) => void import_react_native12.Linking.openURL(url).catch(() => {
5174
- })
6090
+ openExternal: (url) => void import_react_native13.Linking.openURL(url).catch(() => {
6091
+ }),
6092
+ windowSize: () => import_react_native13.Dimensions.get("window")
5175
6093
  };
5176
6094
  }
5177
6095
  function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPayload, flagAttributes, ...options }) {
5178
- const [client, setClient] = (0, import_react9.useState)(null);
5179
- const optionsRef = (0, import_react9.useRef)(options);
6096
+ const [client, setClient] = (0, import_react10.useState)(null);
6097
+ const optionsRef = (0, import_react10.useRef)(options);
5180
6098
  optionsRef.current = options;
5181
- const userRef = (0, import_react9.useRef)(user);
6099
+ const userRef = (0, import_react10.useRef)(user);
5182
6100
  userRef.current = user;
5183
6101
  const flagsOpts = options.flags === false ? null : options.flags ?? {};
5184
6102
  const payload = flagsPayload ?? flagsOpts?.payload ?? null;
5185
- const payloadRef = (0, import_react9.useRef)(payload);
6103
+ const payloadRef = (0, import_react10.useRef)(payload);
5186
6104
  payloadRef.current = payload;
5187
- const appAttributes = (0, import_react9.useMemo)(() => ({ ...flagsOpts?.attributes ?? {}, ...flagAttributes ?? {} }), [(0, import_flags3.stableStringify)(flagsOpts?.attributes ?? {}), (0, import_flags3.stableStringify)(flagAttributes ?? {})]);
5188
- const appAttributesRef = (0, import_react9.useRef)(appAttributes);
6105
+ const appAttributes = (0, import_react10.useMemo)(() => ({ ...flagsOpts?.attributes ?? {}, ...flagAttributes ?? {} }), [(0, import_flags3.stableStringify)(flagsOpts?.attributes ?? {}), (0, import_flags3.stableStringify)(flagAttributes ?? {})]);
6106
+ const appAttributesRef = (0, import_react10.useRef)(appAttributes);
5189
6107
  appAttributesRef.current = appAttributes;
5190
6108
  const { projectId, environment, apiUrl, storage } = options;
5191
- (0, import_react9.useEffect)(() => {
6109
+ (0, import_react10.useEffect)(() => {
5192
6110
  if (!projectId) return;
5193
6111
  let c;
5194
6112
  try {
@@ -5214,7 +6132,7 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
5214
6132
  }
5215
6133
  setClient(c);
5216
6134
  void c.init();
5217
- const sub = import_react_native12.AppState.addEventListener("change", (s) => c.setAppState(s));
6135
+ const sub = import_react_native13.AppState.addEventListener("change", (s) => c.setAppState(s));
5218
6136
  return () => {
5219
6137
  sub.remove();
5220
6138
  c.destroy();
@@ -5222,8 +6140,8 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
5222
6140
  };
5223
6141
  }, [projectId, environment, apiUrl, storage]);
5224
6142
  const userKey = user ? JSON.stringify([user.id, user.traits ?? null, user.account ?? null]) : "";
5225
- const hadUser = (0, import_react9.useRef)(false);
5226
- (0, import_react9.useEffect)(() => {
6143
+ const hadUser = (0, import_react10.useRef)(false);
6144
+ (0, import_react10.useEffect)(() => {
5227
6145
  if (!client) return;
5228
6146
  if (user && user.id) {
5229
6147
  hadUser.current = true;
@@ -5233,16 +6151,21 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
5233
6151
  client.reset();
5234
6152
  }
5235
6153
  }, [client, userKey]);
5236
- (0, import_react9.useEffect)(() => {
6154
+ (0, import_react10.useEffect)(() => {
5237
6155
  client?.flags.setAttributes(appAttributes);
5238
6156
  }, [client, appAttributes]);
6157
+ (0, import_react10.useEffect)(() => {
6158
+ client?.setLocale(options.locale ?? null);
6159
+ }, [client, options.locale]);
5239
6160
  const bootstrap = useBootstrap(payload, { ...user?.id ? { ...user.traits ?? {}, id: user.id } : {}, ...appAttributes });
5240
- const flags = (0, import_react9.useMemo)(() => ({ client: client?.flags ?? null, bootstrap }), [client, bootstrap]);
5241
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(GuidelyContext.Provider, { value: client, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(FlagsContext.Provider, { value: flags, children: [
5242
- children,
5243
- client ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(GuidelyOverlay, { client, insets, launcherOffset }) : null
6161
+ const flags = (0, import_react10.useMemo)(() => ({ client: client?.flags ?? null, bootstrap }), [client, bootstrap]);
6162
+ const tapObserver = useTapObserver(client);
6163
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(GuidelyContext.Provider, { value: client, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(FlagsContext.Provider, { value: flags, children: [
6164
+ options.autocapture === false ? children : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native13.View, { style: styles10.root, ...tapObserver, children }),
6165
+ client ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(GuidelyOverlay, { client, insets, launcherOffset }) : null
5244
6166
  ] }) });
5245
6167
  }
6168
+ var styles10 = import_react_native13.StyleSheet.create({ root: { flex: 1 } });
5246
6169
  var NOOP_RESULTS = {
5247
6170
  init: Promise.resolve(),
5248
6171
  ready: Promise.resolve(),
@@ -5291,12 +6214,12 @@ var noopClient = new Proxy({}, {
5291
6214
  }
5292
6215
  });
5293
6216
  function useGuidely() {
5294
- return (0, import_react9.useContext)(GuidelyContext) ?? noopClient;
6217
+ return (0, import_react10.useContext)(GuidelyContext) ?? noopClient;
5295
6218
  }
5296
6219
  function useGuidelyReady() {
5297
- const client = (0, import_react9.useContext)(GuidelyContext);
5298
- const [ready, setReady] = (0, import_react9.useState)(() => !!client?.isClientReady());
5299
- (0, import_react9.useEffect)(() => {
6220
+ const client = (0, import_react10.useContext)(GuidelyContext);
6221
+ const [ready, setReady] = (0, import_react10.useState)(() => !!client?.isClientReady());
6222
+ (0, import_react10.useEffect)(() => {
5300
6223
  if (!client) return setReady(false);
5301
6224
  setReady(client.isClientReady());
5302
6225
  const off = client.on("ready", () => setReady(true));
@@ -5306,44 +6229,44 @@ function useGuidelyReady() {
5306
6229
  return ready;
5307
6230
  }
5308
6231
  function useGuidelyIdentify(userId, traits, account) {
5309
- const client = (0, import_react9.useContext)(GuidelyContext);
6232
+ const client = (0, import_react10.useContext)(GuidelyContext);
5310
6233
  const key = JSON.stringify([userId ?? null, traits ?? null, account ?? null]);
5311
- (0, import_react9.useEffect)(() => {
6234
+ (0, import_react10.useEffect)(() => {
5312
6235
  if (client && userId) void client.identify(userId, traits, account);
5313
6236
  }, [client, key]);
5314
6237
  }
5315
6238
  function useGuidelyEvent(event, handler) {
5316
- const client = (0, import_react9.useContext)(GuidelyContext);
5317
- const ref = (0, import_react9.useRef)(handler);
6239
+ const client = (0, import_react10.useContext)(GuidelyContext);
6240
+ const ref = (0, import_react10.useRef)(handler);
5318
6241
  ref.current = handler;
5319
- (0, import_react9.useEffect)(() => {
6242
+ (0, import_react10.useEffect)(() => {
5320
6243
  if (!client) return;
5321
6244
  return client.on(event, ((...args) => ref.current(...args)));
5322
6245
  }, [client, event]);
5323
6246
  }
5324
6247
  function useGuidelyScreen(name, properties) {
5325
- const client = (0, import_react9.useContext)(GuidelyContext);
5326
- (0, import_react9.useEffect)(() => {
6248
+ const client = (0, import_react10.useContext)(GuidelyContext);
6249
+ (0, import_react10.useEffect)(() => {
5327
6250
  if (client && name) client.screen(name, properties);
5328
6251
  }, [client, name]);
5329
6252
  }
5330
6253
  function useGuidelyNavigation(navigationRef, options = {}) {
5331
- const client = (0, import_react9.useContext)(GuidelyContext);
5332
- const nameRef = (0, import_react9.useRef)(options.screenName);
6254
+ const client = (0, import_react10.useContext)(GuidelyContext);
6255
+ const nameRef = (0, import_react10.useRef)(options.screenName);
5333
6256
  nameRef.current = options.screenName;
5334
6257
  const report = () => {
5335
6258
  const route = navigationRef.getCurrentRoute?.();
5336
6259
  if (client && route?.name) client.screen(nameRef.current ? nameRef.current(route) : route.name);
5337
6260
  };
5338
- (0, import_react9.useEffect)(report, [client]);
6261
+ (0, import_react10.useEffect)(report, [client]);
5339
6262
  return { onReady: report, onStateChange: report };
5340
6263
  }
5341
6264
  function useUiSelector(select, fallback) {
5342
- const client = (0, import_react9.useContext)(GuidelyContext);
5343
- const selectRef = (0, import_react9.useRef)(select);
6265
+ const client = (0, import_react10.useContext)(GuidelyContext);
6266
+ const selectRef = (0, import_react10.useRef)(select);
5344
6267
  selectRef.current = select;
5345
- const [value, setValue] = (0, import_react9.useState)(() => client ? select(client) : fallback);
5346
- (0, import_react9.useEffect)(() => {
6268
+ const [value, setValue] = (0, import_react10.useState)(() => client ? select(client) : fallback);
6269
+ (0, import_react10.useEffect)(() => {
5347
6270
  if (!client) return;
5348
6271
  let last = JSON.stringify(selectRef.current(client));
5349
6272
  setValue(selectRef.current(client));
@@ -5383,14 +6306,18 @@ function useActiveGuide() {
5383
6306
  FeatureValue,
5384
6307
  FlagsProvider,
5385
6308
  GuidelyClient,
6309
+ GuidelyMask,
5386
6310
  GuidelyOverlay,
5387
6311
  GuidelyProvider,
5388
6312
  GuidelyScreenScope,
5389
6313
  GuidelyTarget,
6314
+ HeatmapBuffer,
5390
6315
  InMemoryStickyBucketStore,
5391
6316
  Messenger,
6317
+ RageClickDetector,
5392
6318
  SDK_NAME,
5393
6319
  SDK_VERSION,
6320
+ TapTracker,
5394
6321
  TargetRegistry,
5395
6322
  createFlagsClient,
5396
6323
  createSseParser,