@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.js CHANGED
@@ -3,9 +3,9 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
 
5
5
  // src/index.tsx
6
- import { useContext as useContext3, useEffect as useEffect9, useMemo as useMemo5, useRef as useRef6, useState as useState8 } from "react";
7
- import { AppState as AppState2, Dimensions, Linking, Platform as Platform6 } from "react-native";
8
- import { createStaticFlags as createStaticFlags2, stableStringify as stableStringify2 } from "@guidelytech/flags";
6
+ import { useContext as useContext3, useEffect as useEffect9, useMemo as useMemo6, useRef as useRef7, useState as useState8 } from "react";
7
+ import { AppState as AppState2, Dimensions, Linking, Platform as Platform6, StyleSheet as StyleSheet10, View as View12 } from "react-native";
8
+ import { createStaticFlags as createStaticFlags2, stableStringify as stableStringify3 } from "@guidelytech/flags";
9
9
 
10
10
  // src/flags.tsx
11
11
  import { createFlagsClient, createStaticFlags, jsonEquals, stableStringify } from "@guidelytech/flags";
@@ -96,7 +96,7 @@ function FlagsProvider({ children, client: external, payload, ...options }) {
96
96
  return /* @__PURE__ */ jsx(FlagsContext.Provider, { value, children });
97
97
  }
98
98
 
99
- // ../shared/dist/chunk-UW6NRYRY.js
99
+ // ../shared/dist/chunk-TGIUEYXM.js
100
100
  function fnv1a(input) {
101
101
  let h = 2166136261;
102
102
  for (let i = 0; i < input.length; i++) {
@@ -276,6 +276,8 @@ function resolveField(rule, ctx) {
276
276
  return ctx.now.getHours();
277
277
  case "rollout.percentage":
278
278
  return rolloutBucket(ctx.userId || ctx.anonymousId, ctx.rolloutSalt || "guidely");
279
+ case "experiment.bucket":
280
+ return rule.key ? rolloutBucket(ctx.userId || ctx.anonymousId, rule.key) : void 0;
279
281
  case "environment":
280
282
  return ctx.environment;
281
283
  case "locale":
@@ -286,6 +288,9 @@ function resolveField(rule, ctx) {
286
288
  return ctx.screen;
287
289
  case "app.version":
288
290
  return ctx.appVersion;
291
+ case "page.views":
292
+ case "feature.clicks":
293
+ return rule.key ? ctx.tagUsage?.[rule.key] ?? 0 : 0;
289
294
  default:
290
295
  return void 0;
291
296
  }
@@ -394,8 +399,8 @@ function sanitizeProperties(props) {
394
399
  for (const [k, v] of Object.entries(props)) {
395
400
  if (SENSITIVE_PROPERTY_PATTERN.test(k)) continue;
396
401
  if (typeof v === "function" || typeof v === "symbol") continue;
397
- const str = typeof v === "string" ? v : JSON.stringify(v ?? null);
398
- size += k.length + (str?.length ?? 0);
402
+ const str2 = typeof v === "string" ? v : JSON.stringify(v ?? null);
403
+ size += k.length + (str2?.length ?? 0);
399
404
  if (size > MAX_PROPERTY_BYTES) break;
400
405
  out[k.slice(0, 100)] = typeof v === "string" ? v.slice(0, 1e3) : v;
401
406
  }
@@ -552,9 +557,151 @@ function isArticleListed(collections, article, member) {
552
557
  }
553
558
  return true;
554
559
  }
560
+ var RC_MODULE_IDS = ["announcements", "search", "checklists", "guides", "articles", "links", "faqs"];
561
+ function orderedModules(modules) {
562
+ const known = (modules ?? []).filter((m, i, all) => RC_MODULE_IDS.includes(m.id) && all.findIndex((x) => x.id === m.id) === i);
563
+ const missing2 = RC_MODULE_IDS.filter((id) => !known.some((m) => m.id === id)).map((id) => ({ id, enabled: true }));
564
+ return [...known, ...missing2];
565
+ }
566
+ function visibleAnnouncements(items, member, now = Date.now()) {
567
+ return (items ?? []).filter((a) => {
568
+ const t = Date.parse(a.publishedAt);
569
+ return !Number.isNaN(t) && t <= now && matchesAudiences(a.audienceIds, member);
570
+ }).sort((a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt));
571
+ }
572
+ function unreadAnnouncements(items, lastSeenAt) {
573
+ const seen = lastSeenAt ? Date.parse(lastSeenAt) : NaN;
574
+ if (Number.isNaN(seen)) return items.length;
575
+ return items.filter((a) => Date.parse(a.publishedAt) > seen).length;
576
+ }
555
577
  var CAPTURE_SCREENSHOT_MAX_BYTES = 8 * 1024 * 1024;
578
+ var DEFAULT_LOCALE = "en";
579
+ function normalizeLocale(input) {
580
+ if (typeof input !== "string") return "";
581
+ const parts = input.trim().replace(/_/g, "-").split("-").filter(Boolean);
582
+ if (!parts.length || !/^[a-zA-Z]{2,3}$/.test(parts[0])) return "";
583
+ return parts.slice(0, 3).map((p, i) => i === 0 ? p.toLowerCase() : p.length === 4 ? p[0].toUpperCase() + p.slice(1).toLowerCase() : p.length === 2 ? p.toUpperCase() : p).join("-");
584
+ }
585
+ function baseLanguage(locale) {
586
+ return normalizeLocale(locale).split("-")[0] ?? "";
587
+ }
588
+ function resolveLocale(requested, available, defaultLocale = DEFAULT_LOCALE) {
589
+ const def = normalizeLocale(defaultLocale) || DEFAULT_LOCALE;
590
+ const avail = available.map(normalizeLocale).filter(Boolean);
591
+ const all = [def, ...avail.filter((a) => a.toLowerCase() !== def.toLowerCase())];
592
+ for (const raw of requested) {
593
+ const want = normalizeLocale(raw);
594
+ if (!want) continue;
595
+ const exact = all.find((a) => a.toLowerCase() === want.toLowerCase());
596
+ if (exact) return exact;
597
+ const base = baseLanguage(want);
598
+ const sameBase = all.find((a) => a.toLowerCase() === base) ?? all.find((a) => baseLanguage(a) === base);
599
+ if (sameBase) return sameBase;
600
+ }
601
+ return def;
602
+ }
603
+ function visitorLocales(explicit, traits, platform = []) {
604
+ const out = [];
605
+ const push = (v) => {
606
+ const n = normalizeLocale(v);
607
+ if (n && !out.includes(n)) out.push(n);
608
+ };
609
+ push(explicit);
610
+ push(traits?.language);
611
+ push(traits?.locale);
612
+ for (const p of platform) push(p);
613
+ return out;
614
+ }
615
+ function guideLocales(config) {
616
+ const t = config.localization?.translations ?? {};
617
+ return Object.keys(t).filter((l) => Object.values(t[l] ?? {}).some((v) => typeof v === "string" && v.trim() !== ""));
618
+ }
619
+ function pick(map, key, fallback) {
620
+ const v = map?.[key];
621
+ return typeof v === "string" && v.trim() !== "" ? v : fallback;
622
+ }
623
+ function applyGuideTranslations(config, map) {
624
+ if (!map || !Object.keys(map).length) return config;
625
+ return {
626
+ ...config,
627
+ steps: config.steps.map((s) => ({
628
+ ...s,
629
+ title: pick(map, `steps.${s.id}.title`, s.title),
630
+ body: pick(map, `steps.${s.id}.body`, s.body),
631
+ customHtml: pick(map, `steps.${s.id}.customHtml`, s.customHtml),
632
+ imageAlt: pick(map, `steps.${s.id}.imageAlt`, s.imageAlt),
633
+ buttons: s.buttons.map((b) => ({ ...b, label: pick(map, `steps.${s.id}.buttons.${b.id}.label`, b.label) }))
634
+ })),
635
+ checklist: config.checklist ? {
636
+ ...config.checklist,
637
+ launcherLabel: pick(map, "checklist.launcherLabel", config.checklist.launcherLabel),
638
+ completionTitle: pick(map, "checklist.completionTitle", config.checklist.completionTitle),
639
+ completionBody: pick(map, "checklist.completionBody", config.checklist.completionBody),
640
+ tasks: config.checklist.tasks.map((t) => ({ ...t, title: pick(map, `checklist.tasks.${t.id}.title`, t.title), description: pick(map, `checklist.tasks.${t.id}.description`, t.description) }))
641
+ } : config.checklist,
642
+ survey: config.survey ? {
643
+ ...config.survey,
644
+ thankYouTitle: pick(map, "survey.thankYouTitle", config.survey.thankYouTitle),
645
+ thankYouBody: pick(map, "survey.thankYouBody", config.survey.thankYouBody),
646
+ questions: config.survey.questions.map((q) => ({
647
+ ...q,
648
+ title: pick(map, `survey.questions.${q.id}.title`, q.title),
649
+ description: pick(map, `survey.questions.${q.id}.description`, q.description),
650
+ placeholder: pick(map, `survey.questions.${q.id}.placeholder`, q.placeholder),
651
+ lowLabel: pick(map, `survey.questions.${q.id}.lowLabel`, q.lowLabel),
652
+ highLabel: pick(map, `survey.questions.${q.id}.highLabel`, q.highLabel),
653
+ options: q.options.map((o) => ({ ...o, label: pick(map, `survey.questions.${q.id}.options.${o.id}.label`, o.label) }))
654
+ }))
655
+ } : config.survey
656
+ };
657
+ }
658
+ function localizeGuide(guide, requested, defaultLocale = DEFAULT_LOCALE) {
659
+ const translations = guide.config.localization?.translations ?? {};
660
+ const locale = resolveLocale(requested, guideLocales(guide.config), defaultLocale);
661
+ const key = Object.keys(translations).find((l) => normalizeLocale(l).toLowerCase() === locale.toLowerCase());
662
+ if (!key) return { ...guide, locale };
663
+ return { ...guide, locale, config: applyGuideTranslations(guide.config, translations[key]) };
664
+ }
665
+ function resourceCenterLocales(rc) {
666
+ const set = new Set(Object.keys(rc.translations ?? {}));
667
+ for (const a of rc.articles) for (const l of Object.keys(a.translations ?? {})) set.add(l);
668
+ for (const a of rc.announcements ?? []) for (const l of Object.keys(a.translations ?? {})) set.add(l);
669
+ return [...set];
670
+ }
671
+ function translationFor(map, locale) {
672
+ if (!map) return void 0;
673
+ const key = Object.keys(map).find((l) => normalizeLocale(l).toLowerCase() === locale.toLowerCase());
674
+ return key ? map[key] : void 0;
675
+ }
676
+ function localizeArticle(a, locale) {
677
+ const t = translationFor(a.translations, locale);
678
+ if (!t) return a;
679
+ const body = t.body?.trim() ? t.body : a.body;
680
+ return { ...a, title: t.title?.trim() ? t.title : a.title, summary: t.summary?.trim() ? t.summary : a.summary, body, ...t.bodyOmitted ? { bodyOmitted: true } : {} };
681
+ }
682
+ function localizeAnnouncement(a, locale) {
683
+ const t = translationFor(a.translations, locale);
684
+ if (!t) return a;
685
+ return { ...a, title: t.title?.trim() ? t.title : a.title, body: t.body?.trim() ? t.body : a.body };
686
+ }
687
+ function localizeResourceCenter(rc, requested, defaultLocale = DEFAULT_LOCALE) {
688
+ const locale = resolveLocale(requested, resourceCenterLocales(rc), defaultLocale);
689
+ if (locale.toLowerCase() === normalizeLocale(defaultLocale).toLowerCase()) return { ...rc, locale };
690
+ const map = translationFor(rc.translations, locale);
691
+ return {
692
+ ...rc,
693
+ locale,
694
+ title: pick(map, "title", rc.title),
695
+ launcher: { ...rc.launcher, label: pick(map, "launcher.label", rc.launcher.label) },
696
+ collections: rc.collections.map((c) => ({ ...c, name: pick(map, `collections.${c.id}.name`, c.name), description: pick(map, `collections.${c.id}.description`, c.description) })),
697
+ links: rc.links.map((l) => ({ ...l, label: pick(map, `links.${l.id}.label`, l.label) })),
698
+ faqs: rc.faqs.map((f) => ({ ...f, question: pick(map, `faqs.${f.id}.question`, f.question), answer: pick(map, `faqs.${f.id}.answer`, f.answer) })),
699
+ articles: rc.articles.map((a) => localizeArticle(a, locale)),
700
+ announcements: rc.announcements?.map((a) => localizeAnnouncement(a, locale))
701
+ };
702
+ }
556
703
 
557
- // ../sdk-core/dist/chunk-C4WWSF67.js
704
+ // ../sdk-core/dist/chunk-JUSM7J5N.js
558
705
  var __defProp2 = Object.defineProperty;
559
706
  var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
560
707
  var __publicField2 = (obj, key, value) => __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -852,6 +999,8 @@ var KEYS = {
852
999
  queue: (key) => `guidely:queue:${key}`,
853
1000
  /** Survey answers and guide/checklist progress not yet accepted by the server. */
854
1001
  outbox: (key) => `guidely:outbox:${key}`,
1002
+ /** Agent Analytics turns not yet accepted by the server. */
1003
+ agentOutbox: (key) => `guidely:agents:${key}`,
855
1004
  session: "guidely:session",
856
1005
  active: "guidely:active",
857
1006
  authoring: "guidely:authoring",
@@ -864,7 +1013,7 @@ var KEYS = {
864
1013
  newsSeen: (key) => `guidely:news:${key}`
865
1014
  };
866
1015
 
867
- // ../sdk-core/dist/chunk-GDJB466J.js
1016
+ // ../sdk-core/dist/chunk-ZAF2KRJA.js
868
1017
  var EventQueue = class {
869
1018
  constructor(opts) {
870
1019
  __publicField2(this, "opts", opts);
@@ -1140,12 +1289,32 @@ function areMutuallyExclusive(a, b) {
1140
1289
  return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
1141
1290
  }
1142
1291
  function isWithinSchedule(guide, now = Date.now()) {
1143
- const s = guide.config.survey;
1144
- if (!s) return true;
1145
- if (s.startAt && now < Date.parse(s.startAt)) return false;
1146
- if (s.endAt && now > Date.parse(s.endAt)) return false;
1292
+ for (const w of [guide.config.schedule, guide.config.survey]) {
1293
+ if (!w) continue;
1294
+ if (w.startAt && now < Date.parse(w.startAt)) return false;
1295
+ if (w.endAt && now > Date.parse(w.endAt)) return false;
1296
+ }
1147
1297
  return true;
1148
1298
  }
1299
+ var MINUTE = 60 * 1e3;
1300
+ function countsTowardThrottle(guide) {
1301
+ return isExclusiveGuide(guide) && !guide.config.frequency.ignoreThrottling;
1302
+ }
1303
+ function isThrottleEligible(settings, launches, now = Date.now()) {
1304
+ if (!settings || !settings.enabled) return true;
1305
+ const times = launches.map((l) => Date.parse(l)).filter((t) => !Number.isNaN(t));
1306
+ const windowMs = Math.max(1, settings.windowHours) * HOUR;
1307
+ if (times.filter((t) => now - t < windowMs).length >= Math.max(1, settings.maxGuides)) return false;
1308
+ if (settings.minGapMinutes > 0 && times.some((t) => now - t < settings.minGapMinutes * MINUTE)) return false;
1309
+ return true;
1310
+ }
1311
+ function pruneLaunches(launches, settings, now = Date.now()) {
1312
+ const keepMs = Math.max(settings?.windowHours ?? 24, 1) * HOUR + (settings?.minGapMinutes ?? 0) * MINUTE;
1313
+ return launches.filter((l) => {
1314
+ const t = Date.parse(l);
1315
+ return !Number.isNaN(t) && now - t < keepMs;
1316
+ }).slice(-100);
1317
+ }
1149
1318
  function dismissedAfterMerge(local, server) {
1150
1319
  if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
1151
1320
  return local?.dismissedAt ?? server.dismissedAt ?? null;
@@ -1154,6 +1323,115 @@ function dismissedAfterMerge(local, server) {
1154
1323
  // src/core/client.ts
1155
1324
  import { createFlagsClient as createFlagsClient2 } from "@guidelytech/flags";
1156
1325
 
1326
+ // src/core/autocapture.ts
1327
+ var TAP_SLOP = 10;
1328
+ var TAP_MAX_MS = 1e3;
1329
+ var HEATMAP_MAX_POINTS = 40;
1330
+ function quoteValue(value) {
1331
+ return `"${value.replace(/["\\]/g, "\\$&")}"`;
1332
+ }
1333
+ function attrSelector(name, value) {
1334
+ return `[${name}=${quoteValue(value)}]`.slice(0, 300);
1335
+ }
1336
+ function cleanText(text) {
1337
+ const t = (text ?? "").replace(/\s+/g, " ").trim();
1338
+ return t ? t.slice(0, 100) : void 0;
1339
+ }
1340
+ function tapProperties(el, x, y, platform, maskAll) {
1341
+ const props = { selector: el.selector.slice(0, 300), tag: el.tag || "View", x: Math.round(x) || 0, y: Math.round(y) || 0, platform };
1342
+ const text = maskAll ? void 0 : cleanText(el.text);
1343
+ if (text) props.text = text;
1344
+ return props;
1345
+ }
1346
+ var r2 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 100) / 100;
1347
+ function heatmapPoint(x, y, el, rect, rage) {
1348
+ const p = { k: rage ? "r" : "c", x: Math.round(x) || 0, y: Math.round(y) || 0 };
1349
+ const s = el?.selector.slice(0, 120);
1350
+ if (s) p.s = s;
1351
+ if (s && rect && rect.width > 0 && rect.height > 0) {
1352
+ p.rx = r2((x - rect.x) / rect.width);
1353
+ p.ry = r2((y - rect.y) / rect.height);
1354
+ }
1355
+ return p;
1356
+ }
1357
+ var RageClickDetector = class {
1358
+ constructor(threshold = 3, windowMs = 1e3, radius = 30) {
1359
+ __publicField(this, "threshold", threshold);
1360
+ __publicField(this, "windowMs", windowMs);
1361
+ __publicField(this, "radius", radius);
1362
+ __publicField(this, "clicks", []);
1363
+ __publicField(this, "fired", false);
1364
+ }
1365
+ /** Taps in the current burst once it has fired (rage), otherwise 0. */
1366
+ get burst() {
1367
+ return this.fired ? this.clicks.length : 0;
1368
+ }
1369
+ /** Records a tap; returns the tap count when it completes a burst, otherwise 0. */
1370
+ add(x, y, t) {
1371
+ const first = this.clicks[0];
1372
+ const last = this.clicks[this.clicks.length - 1];
1373
+ if (!first || !last || t - last.t > this.windowMs || Math.hypot(x - first.x, y - first.y) > this.radius) {
1374
+ this.clicks = [];
1375
+ this.fired = false;
1376
+ }
1377
+ this.clicks = this.clicks.filter((c) => t - c.t <= this.windowMs);
1378
+ this.clicks.push({ x, y, t });
1379
+ if (this.fired || this.clicks.length < this.threshold) return 0;
1380
+ this.fired = true;
1381
+ return this.clicks.length;
1382
+ }
1383
+ };
1384
+ var HeatmapBuffer = class {
1385
+ constructor() {
1386
+ /** Screen path the buffered points belong to. */
1387
+ __publicField(this, "url", "");
1388
+ __publicField(this, "points", []);
1389
+ }
1390
+ /** Adds a tap (`burst` > 0 marks it and the burst's previous taps as rage); true when the batch is full. */
1391
+ add(url, point, burst) {
1392
+ this.url = url;
1393
+ for (let i = Math.max(0, this.points.length - burst + 1); i < this.points.length; i++) this.points[i].k = "r";
1394
+ this.points.push(point);
1395
+ return this.points.length >= HEATMAP_MAX_POINTS;
1396
+ }
1397
+ /** Empties the buffer, returning what was in it. */
1398
+ take() {
1399
+ const points = this.points;
1400
+ this.points = [];
1401
+ return points.length ? { url: this.url, points } : null;
1402
+ }
1403
+ };
1404
+ var TapTracker = class {
1405
+ constructor() {
1406
+ __publicField(this, "start", null);
1407
+ __publicField(this, "moved", 0);
1408
+ }
1409
+ /** A finger went down. A second finger while one is tracked cancels the gesture (not a tap). */
1410
+ begin(x, y, t, touches, id) {
1411
+ if (this.start || touches > 1) {
1412
+ this.start = null;
1413
+ return;
1414
+ }
1415
+ this.start = { x, y, t, id };
1416
+ this.moved = 0;
1417
+ }
1418
+ move(x, y, id) {
1419
+ const s = this.start;
1420
+ if (s && (id === void 0 || s.id === void 0 || id === s.id)) this.moved = Math.max(this.moved, Math.hypot(x - s.x, y - s.y));
1421
+ }
1422
+ /** The finger lifted: the tap position when it moved ≤ 10pt and lasted < 1s, else null. */
1423
+ end(x, y, t, id) {
1424
+ const s = this.start;
1425
+ if (!s || id !== void 0 && s.id !== void 0 && id !== s.id) return null;
1426
+ this.start = null;
1427
+ const moved = Math.max(this.moved, Math.hypot(x - s.x, y - s.y));
1428
+ return moved <= TAP_SLOP && t - s.t < TAP_MAX_MS ? { x, y } : null;
1429
+ }
1430
+ cancel() {
1431
+ this.start = null;
1432
+ }
1433
+ };
1434
+
1157
1435
  // src/core/targets.ts
1158
1436
  var TargetTimeoutError = class extends Error {
1159
1437
  constructor(ids) {
@@ -1256,6 +1534,35 @@ var TargetRegistry = class {
1256
1534
  return null;
1257
1535
  }
1258
1536
  }
1537
+ /**
1538
+ * The registered target on the current screen whose visible frame contains the point (window
1539
+ * coordinates); the smallest one when targets are nested. Frames that do not answer within
1540
+ * `timeoutMs` are skipped.
1541
+ */
1542
+ async hitTest(x, y, timeoutMs = 500) {
1543
+ const candidates = [];
1544
+ this.targets.forEach((list, id) => {
1545
+ for (const handle of list) if (!(handle.screen && this.screen && handle.screen !== this.screen)) candidates.push({ id, handle });
1546
+ });
1547
+ if (!candidates.length) return null;
1548
+ const rects = await Promise.all(
1549
+ candidates.map(
1550
+ ({ handle }) => new Promise((resolve) => {
1551
+ const timer = setTimeout(() => resolve(null), timeoutMs);
1552
+ void this.measureHandle(handle).then((r) => {
1553
+ clearTimeout(timer);
1554
+ resolve(r);
1555
+ });
1556
+ })
1557
+ )
1558
+ );
1559
+ let best = null;
1560
+ rects.forEach((r, i) => {
1561
+ if (!r || x < r.x || y < r.y || x > r.x + r.width || y > r.y + r.height) return;
1562
+ if (!best || r.width * r.height < best.rect.width * best.rect.height) best = { id: candidates[i].id, rect: r };
1563
+ });
1564
+ return best;
1565
+ }
1259
1566
  /** First of `ids` that is mounted and visible. */
1260
1567
  async resolve(ids) {
1261
1568
  for (const id of ids) {
@@ -2013,7 +2320,8 @@ var Messenger = class {
2013
2320
  this.stopThreadPolling();
2014
2321
  this.set({ open: true, view: { kind: "article", articleId: id, from }, article: null });
2015
2322
  try {
2016
- const res = await this.request("GET", `/articles/${encodeURIComponent(id)}`);
2323
+ const locale = this.deps.locale?.();
2324
+ const res = await this.request("GET", `/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : {});
2017
2325
  this.set({ article: res.article });
2018
2326
  } catch (err) {
2019
2327
  this.deps.onError(err);
@@ -2128,7 +2436,7 @@ var PersistentStore = class {
2128
2436
 
2129
2437
  // src/core/client.ts
2130
2438
  var SDK_NAME = "@guidelytech/react-native";
2131
- var SDK_VERSION = "0.10.0";
2439
+ var SDK_VERSION = "0.11.0";
2132
2440
  var DEFAULT_API_URL = "https://guidelytech.com";
2133
2441
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
2134
2442
  var CACHED_START_MS = 1500;
@@ -2142,6 +2450,8 @@ function settlesWithin(promise, ms) {
2142
2450
  promise.then(done, done);
2143
2451
  });
2144
2452
  }
2453
+ var appVersionKey = (key) => `guidely:app:${key}`;
2454
+ var UNKNOWN_VIEW = { selector: "View", tag: "View" };
2145
2455
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
2146
2456
  var emptyState = () => ({ guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] });
2147
2457
  var newSession = () => ({ id: `s_${randomId(16)}`, startedAt: nowIso(), lastActiveAt: nowIso(), shownGuides: [], events: [] });
@@ -2173,6 +2483,7 @@ var GuidelyClient = class {
2173
2483
  __publicField(this, "activeSurvey", null);
2174
2484
  __publicField(this, "queued", []);
2175
2485
  __publicField(this, "flows", /* @__PURE__ */ new Map());
2486
+ /** Checklists on screen, with the guide as rendered (in the visitor's language). */
2176
2487
  __publicField(this, "checklists", /* @__PURE__ */ new Map());
2177
2488
  __publicField(this, "expanded", /* @__PURE__ */ new Set());
2178
2489
  __publicField(this, "surveyView", null);
@@ -2192,6 +2503,27 @@ var GuidelyClient = class {
2192
2503
  __publicField(this, "flags");
2193
2504
  /** Experiments already reported this session (`experiment_viewed` is sent once per session). */
2194
2505
  __publicField(this, "exposures", /* @__PURE__ */ new Set());
2506
+ // Product analytics (autocapture).
2507
+ __publicField(this, "rage", new RageClickDetector());
2508
+ __publicField(this, "heatmap", new HeatmapBuffer());
2509
+ /** Taps are processed in order (target frames are measured asynchronously). */
2510
+ __publicField(this, "tapChain", Promise.resolve());
2511
+ /** When the current screen's foreground measurement started; `screenLeft` once it has been reported. */
2512
+ __publicField(this, "screenStartedAt", 0);
2513
+ __publicField(this, "screenLeft", true);
2514
+ /** Last `app_open` (cold start or return to the foreground), for `app_background.foregroundMs`. */
2515
+ __publicField(this, "openedAt", Date.now());
2516
+ __publicField(this, "launchChecked", false);
2517
+ /** An identity was stored before this launch (an existing install adopting lifecycle events). */
2518
+ __publicField(this, "hadIdentity", false);
2519
+ /** Language set with `setLocale()` (or the `locale` option). */
2520
+ __publicField(this, "locale", null);
2521
+ /** Language each shown guide was rendered in; added to its events as `locale`. */
2522
+ __publicField(this, "guideLocales", /* @__PURE__ */ new Map());
2523
+ /** The resource center in the visitor's language, recomputed when the config or language changes. */
2524
+ __publicField(this, "localizedRc", null);
2525
+ /** "What's new" last-seen time when the resource center opened (entries after it read as new). */
2526
+ __publicField(this, "announcementsSeenAtOpen", null);
2195
2527
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
2196
2528
  __publicField(this, "eventBus", {
2197
2529
  on: (name, cb) => {
@@ -2221,6 +2553,7 @@ var GuidelyClient = class {
2221
2553
  if (!options.storage) this.log("no `storage` passed: identity, guide history, queued events and survey answers last only for this app session, so whatever could not be sent during a Guidely outage is lost when the app restarts. Pass AsyncStorage to persist them.");
2222
2554
  this.cleanups.push(this.targets.onTap((id) => this.handleTap(id)));
2223
2555
  if (options.user) this.user = { ...options.user };
2556
+ this.locale = options.locale ? normalizeLocale(options.locale) || null : null;
2224
2557
  this.flags = this.createFlags(options.flags);
2225
2558
  }
2226
2559
  createFlags(opts) {
@@ -2269,9 +2602,10 @@ var GuidelyClient = class {
2269
2602
  async bootstrap() {
2270
2603
  const key = this.options.projectId;
2271
2604
  try {
2272
- 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)]);
2605
+ await this.store.hydrate([KEYS.anon, KEYS.seen, KEYS.session, KEYS.config(key), KEYS.etag(key), KEYS.state(key), KEYS.user(key), KEYS.queue(key), KEYS.outbox(key), KEYS.conversations(key), appVersionKey(key)]);
2273
2606
  if (this.destroyed) return;
2274
2607
  this.anonymousId = this.store.get(KEYS.anon) || "";
2608
+ this.hadIdentity = !!this.anonymousId;
2275
2609
  if (!this.anonymousId) {
2276
2610
  this.anonymousId = `anon_${randomId(20)}`;
2277
2611
  this.store.set(KEYS.anon, this.anonymousId);
@@ -2308,6 +2642,7 @@ var GuidelyClient = class {
2308
2642
  if (!this.config) await fresh;
2309
2643
  else if (!await settlesWithin(fresh, CACHED_START_MS)) void fresh.then(() => this.applyConfigChange(before));
2310
2644
  if (this.destroyed) return;
2645
+ this.checkLaunch();
2311
2646
  this.wireTimers();
2312
2647
  if (this.user?.id) await settlesWithin(this.identify(this.user.id, this.user.traits, this.user.account), CACHED_START_MS);
2313
2648
  if (this.destroyed) return;
@@ -2337,6 +2672,7 @@ var GuidelyClient = class {
2337
2672
  if (res.etag) this.store.set(KEYS.etag(this.options.projectId), res.etag);
2338
2673
  else this.store.remove(KEYS.etag(this.options.projectId));
2339
2674
  this.syncMessenger();
2675
+ if (this.hydrated) this.checkLaunch();
2340
2676
  this.emitter.emit("config", this.config);
2341
2677
  this.log("config loaded", this.config.configVersion, `${this.config.guides.length} guides`);
2342
2678
  return true;
@@ -2357,7 +2693,7 @@ var GuidelyClient = class {
2357
2693
  if (!this.isReady || !this.config || this.backgroundedAt !== null) return;
2358
2694
  const url = this.currentUrl();
2359
2695
  const ctx = this.getContext();
2360
- for (const guide of this.config.guides) if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
2696
+ for (const guide of this.candidates()) if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
2361
2697
  this.drainQueue(url, ctx);
2362
2698
  }, 1e3);
2363
2699
  this.cleanups.push(() => clearInterval(poll));
@@ -2381,7 +2717,9 @@ var GuidelyClient = class {
2381
2717
  setAppState(state) {
2382
2718
  if (this.destroyed || !this.hydrated) return;
2383
2719
  if (state === "background") {
2720
+ const wasForeground = this.backgroundedAt === null;
2384
2721
  this.backgroundedAt = Date.now();
2722
+ if (wasForeground) this.wentToBackground();
2385
2723
  this.session.lastActiveAt = nowIso();
2386
2724
  this.persistSession();
2387
2725
  void this.queue?.flush();
@@ -2398,6 +2736,7 @@ var GuidelyClient = class {
2398
2736
  this.persistSession();
2399
2737
  this.firedDelays.clear();
2400
2738
  }
2739
+ this.cameToForeground();
2401
2740
  void this.flags.refresh();
2402
2741
  if (Date.now() - this.configFetchedAt > this.options.configRefreshMs && this.api.health.canSend()) void this.refresh();
2403
2742
  else if (this.isReady) this.evaluate("ready");
@@ -2410,6 +2749,7 @@ var GuidelyClient = class {
2410
2749
  /** Tears everything down. Create a new client to start again. */
2411
2750
  destroy() {
2412
2751
  if (this.destroyed) return;
2752
+ this.flushHeatmap();
2413
2753
  this.destroyed = true;
2414
2754
  this.active?.controller.destroy();
2415
2755
  this.active = null;
@@ -2461,6 +2801,7 @@ var GuidelyClient = class {
2461
2801
  for (const [id, s] of Object.entries(data.guideState || {})) {
2462
2802
  const local = guides[id];
2463
2803
  guides[id] = {
2804
+ ...local,
2464
2805
  impressions: Math.max(local?.impressions ?? 0, s.impressions ?? 0),
2465
2806
  completedAt: local?.completedAt ?? s.completedAt ?? null,
2466
2807
  dismissedAt: local?.dismissedAt ?? s.dismissedAt ?? null,
@@ -2482,7 +2823,10 @@ var GuidelyClient = class {
2482
2823
  checklists,
2483
2824
  surveysSubmitted: Array.from(/* @__PURE__ */ new Set([...this.state.surveysSubmitted, ...data.surveysSubmitted || []])),
2484
2825
  audienceIds: data.audienceIds || [],
2485
- firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt
2826
+ firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt,
2827
+ // Device-local only (the server doesn't hold them).
2828
+ ...this.state.throttle ? { throttle: this.state.throttle } : {},
2829
+ ...this.state.announcementsSeenAt ? { announcementsSeenAt: this.state.announcementsSeenAt } : {}
2486
2830
  };
2487
2831
  if (data.firstSeenAt && Date.now() - Date.parse(data.firstSeenAt) > 6e4) this.firstVisit = false;
2488
2832
  this.persistState();
@@ -2574,6 +2918,8 @@ var GuidelyClient = class {
2574
2918
  ...extra
2575
2919
  };
2576
2920
  if (extra.properties) event.properties = sanitizeProperties(extra.properties);
2921
+ const locale = extra.guideId ? this.guideLocales.get(extra.guideId) : void 0;
2922
+ if (locale) event.properties = { ...event.properties ?? {}, locale };
2577
2923
  this.queue.enqueue(event);
2578
2924
  this.emitter.emit("track", event);
2579
2925
  return event;
@@ -2606,11 +2952,19 @@ var GuidelyClient = class {
2606
2952
  if (!name) return;
2607
2953
  this.whenHydrated(() => {
2608
2954
  const changed = name !== this.screenName;
2955
+ if (changed) {
2956
+ this.leaveScreen();
2957
+ this.flushHeatmap();
2958
+ }
2609
2959
  this.screenName = name;
2610
2960
  this.targets.setScreen(name);
2611
2961
  if (changed) this.syncFlagAttributes();
2612
2962
  const url = this.currentUrl();
2613
- if (changed) this.enqueue("page_view", { properties: { ...properties, path: url, screen: name } });
2963
+ if (changed) {
2964
+ this.screenStartedAt = Date.now();
2965
+ this.screenLeft = this.backgroundedAt !== null;
2966
+ if (this.pageviewsOn()) this.enqueue("page_view", { properties: { ...properties, path: url, screen: name, platform: this.bridge.device.platform } });
2967
+ }
2614
2968
  this.completeChecklistTasksBy("url_visited", url);
2615
2969
  this.active?.controller.resume();
2616
2970
  this.flows.forEach((f) => f.controller.resume());
@@ -2638,6 +2992,149 @@ var GuidelyClient = class {
2638
2992
  if (this.isReady) this.evaluate("click", { targetId });
2639
2993
  }
2640
2994
  // ---------------------------------------------------------------------------
2995
+ // Product analytics (autocapture)
2996
+ // ---------------------------------------------------------------------------
2997
+ captureAllowed() {
2998
+ return !this.destroyed && this.analyticsEnabled();
2999
+ }
3000
+ /** A capability runs when the project enables it (absent on old servers → off), no option turns it off and analytics are allowed. */
3001
+ autocaptureOn(key) {
3002
+ const o = this.options.autocapture;
3003
+ return !!this.config?.settings?.autocapture?.[key] && o !== false && !(typeof o === "object" && o?.[key] === false) && this.captureAllowed();
3004
+ }
3005
+ /** `page_view` from `screen()` predates autocapture: only an explicit opt-out stops it. */
3006
+ pageviewsOn() {
3007
+ const o = this.options.autocapture;
3008
+ return this.config?.settings?.autocapture?.pageviews !== false && o !== false && !(typeof o === "object" && o?.pageviews === false);
3009
+ }
3010
+ /** True when taps are worth observing (clicks, rage taps or heatmaps are on); the provider skips resolving views otherwise. */
3011
+ isTapCaptureOn() {
3012
+ return this.hydrated && (this.autocaptureOn("clicks") || this.autocaptureOn("rageClicks") || this.autocaptureOn("heatmaps"));
3013
+ }
3014
+ /**
3015
+ * Records a tap observed by the provider: `click` for an interactive element, `rage_click` for
3016
+ * bursts and a heatmap point. A registered `<GuidelyTarget>` whose frame contains the point
3017
+ * (smallest wins) takes precedence over the resolved element and is reported as `#<id>`.
3018
+ */
3019
+ recordTap(tap) {
3020
+ try {
3021
+ if (!this.hydrated || this.destroyed || !Number.isFinite(tap.x) || !Number.isFinite(tap.y)) return;
3022
+ const on = { clicks: this.autocaptureOn("clicks"), rage: this.autocaptureOn("rageClicks"), heat: this.autocaptureOn("heatmaps") };
3023
+ if (!on.clicks && !on.rage && !on.heat) return;
3024
+ const t = tap.t ?? Date.now();
3025
+ const url = this.screenName ? this.currentUrl() : "";
3026
+ this.tapChain = this.tapChain.then(() => this.processTap(tap, t, url, on)).catch((err) => this.reportError(err));
3027
+ } catch (err) {
3028
+ this.reportError(err);
3029
+ }
3030
+ }
3031
+ async processTap(tap, t, url, on) {
3032
+ let el = tap.element ?? null;
3033
+ let rect = null;
3034
+ const target = this.targets.ids().length ? await this.targets.hitTest(tap.x, tap.y) : null;
3035
+ if (target) {
3036
+ el = { selector: `#${target.id}`, tag: el?.tag ?? tap.hit?.tag ?? "View", text: el?.text };
3037
+ rect = target.rect;
3038
+ }
3039
+ if (this.destroyed) return;
3040
+ const platform = this.bridge.device.platform;
3041
+ const maskAll = !!this.config?.settings?.replay?.maskAllText;
3042
+ const eventUrl = this.collectUrls() && url ? url : void 0;
3043
+ if (on.clicks && el) this.enqueue("click", { url: eventUrl, properties: { ...tapProperties(el, tap.x, tap.y, platform, maskAll) } });
3044
+ const count = this.rage.add(tap.x, tap.y, t);
3045
+ if (on.rage && count) this.enqueue("rage_click", { url: eventUrl, properties: { ...tapProperties(el ?? tap.hit ?? UNKNOWN_VIEW, tap.x, tap.y, platform, maskAll), count } });
3046
+ if (!on.heat) return;
3047
+ const burst = this.rage.burst;
3048
+ if (el && !rect && el.measure) rect = await el.measure().catch(() => null);
3049
+ if (this.destroyed) return;
3050
+ if (this.heatmap.url !== url) this.flushHeatmap();
3051
+ if (this.heatmap.add(url, heatmapPoint(tap.x, tap.y, el, rect, burst > 0), burst)) this.flushHeatmap();
3052
+ }
3053
+ /** Sends the buffered heatmap taps as one `heatmap` event attributed to the screen they happened on. */
3054
+ flushHeatmap() {
3055
+ const batch = this.heatmap.take();
3056
+ if (!batch || !this.captureAllowed()) return;
3057
+ const collect = this.collectUrls() && !!batch.url;
3058
+ const { width, height } = this.windowSize();
3059
+ this.enqueue("heatmap", {
3060
+ url: collect ? batch.url : void 0,
3061
+ properties: { ...collect ? { path: batch.url } : {}, viewportWidth: width, viewportHeight: height, pageHeight: height, platform: this.bridge.device.platform, points: batch.points }
3062
+ });
3063
+ }
3064
+ windowSize() {
3065
+ try {
3066
+ const s = this.bridge.windowSize?.();
3067
+ if (s && Number.isFinite(s.width) && Number.isFinite(s.height)) return { width: Math.round(s.width), height: Math.round(s.height) };
3068
+ } catch {
3069
+ }
3070
+ return { width: 0, height: 0 };
3071
+ }
3072
+ /** Ends the foreground measurement of the current screen with a `page_leave` (once per measurement). */
3073
+ leaveScreen() {
3074
+ if (!this.screenName || this.screenLeft) return;
3075
+ this.screenLeft = true;
3076
+ if (!this.autocaptureOn("pageLeave")) return;
3077
+ const url = this.currentUrl();
3078
+ const collect = this.collectUrls();
3079
+ this.enqueue("page_leave", {
3080
+ url: collect ? url : void 0,
3081
+ properties: { ...collect ? { path: url } : {}, screen: this.screenName, durationMs: Math.max(0, Date.now() - this.screenStartedAt), scrollDepth: 100, platform: this.bridge.device.platform }
3082
+ });
3083
+ }
3084
+ /** `{ appVersion, build }` of this launch, without unknown keys. */
3085
+ appInfo() {
3086
+ const appVersion = this.options.appVersion ?? this.bridge.device.appVersion;
3087
+ const build = this.options.appBuild;
3088
+ return { ...appVersion ? { appVersion: String(appVersion) } : {}, ...build ? { build: String(build) } : {} };
3089
+ }
3090
+ /**
3091
+ * Once per launch, as soon as a config is known: `app_install` / `app_update` from the stored
3092
+ * version (when the app passes `appVersion` and `storage`), then `app_open` with `coldStart: true`.
3093
+ */
3094
+ checkLaunch() {
3095
+ if (this.launchChecked || !this.config || this.destroyed || !this.hydrated) return;
3096
+ this.launchChecked = true;
3097
+ try {
3098
+ const on = this.autocaptureOn("appLifecycle");
3099
+ const info = this.appInfo();
3100
+ if (info.appVersion && this.options.storage) {
3101
+ const key = appVersionKey(this.options.projectId);
3102
+ const stored = readJson(this.store, key, null);
3103
+ if (!stored?.appVersion) {
3104
+ if (!this.hadIdentity && on) this.enqueue("app_install", { properties: { ...info } });
3105
+ } else if (stored.appVersion !== info.appVersion || !!stored.build && !!info.build && stored.build !== info.build) {
3106
+ if (on) this.enqueue("app_update", { properties: { ...info, previousVersion: stored.appVersion, ...stored.build ? { previousBuild: stored.build } : {} } });
3107
+ }
3108
+ writeJson(this.store, key, info);
3109
+ }
3110
+ if (on) this.enqueue("app_open", { properties: { ...info, coldStart: true } });
3111
+ } catch (err) {
3112
+ this.reportError(err);
3113
+ }
3114
+ }
3115
+ wentToBackground() {
3116
+ try {
3117
+ this.leaveScreen();
3118
+ this.flushHeatmap();
3119
+ if (this.launchChecked && this.autocaptureOn("appLifecycle")) this.enqueue("app_background", { properties: { foregroundMs: Math.max(0, Date.now() - this.openedAt) } });
3120
+ } catch (err) {
3121
+ this.reportError(err);
3122
+ }
3123
+ }
3124
+ cameToForeground() {
3125
+ try {
3126
+ this.openedAt = Date.now();
3127
+ if (this.launchChecked && this.autocaptureOn("appLifecycle")) this.enqueue("app_open", { properties: { ...this.appInfo(), coldStart: false } });
3128
+ if (this.screenName && this.screenLeft) {
3129
+ this.screenStartedAt = Date.now();
3130
+ this.screenLeft = false;
3131
+ if (this.pageviewsOn() && this.autocaptureOn("pageLeave")) this.enqueue("page_view", { properties: { path: this.currentUrl(), screen: this.screenName, platform: this.bridge.device.platform, resumed: true } });
3132
+ }
3133
+ } catch (err) {
3134
+ this.reportError(err);
3135
+ }
3136
+ }
3137
+ // ---------------------------------------------------------------------------
2641
3138
  // Targeting + triggers
2642
3139
  // ---------------------------------------------------------------------------
2643
3140
  /** Builds the targeting context for the current moment. */
@@ -2684,6 +3181,50 @@ var GuidelyClient = class {
2684
3181
  if (guide.kind === "checklist") return !this.state.checklists[guide.id]?.dismissedAt;
2685
3182
  return isFrequencyEligible(guide.config.frequency, this.state.guides[guide.id], this.session.shownGuides.includes(guide.id));
2686
3183
  }
3184
+ /** Guide throttling: may this guide launch automatically now? */
3185
+ throttleAllows(guide) {
3186
+ if (!countsTowardThrottle(guide)) return true;
3187
+ return isThrottleEligible(this.config?.settings.throttling, this.state.throttle ?? []);
3188
+ }
3189
+ recordLaunch(guide, manual) {
3190
+ if (manual || !countsTowardThrottle(guide) || !this.config?.settings.throttling?.enabled) return;
3191
+ this.state.throttle = pruneLaunches([...this.state.throttle ?? [], nowIso()], this.config.settings.throttling);
3192
+ }
3193
+ /** Guides and A/B test control groups, which are evaluated the same way but never render. */
3194
+ candidates() {
3195
+ const guides = this.config?.guides ?? [];
3196
+ const holdouts = this.config?.guideHoldouts ?? [];
3197
+ return holdouts.length ? [...guides, ...holdouts.map((h) => h.guide)] : guides;
3198
+ }
3199
+ holdoutFor(guide) {
3200
+ return this.config?.guideHoldouts?.find((h) => h.guide.id === guide.id) ?? null;
3201
+ }
3202
+ // ---------------------------------------------------------------------------
3203
+ // Localization
3204
+ // ---------------------------------------------------------------------------
3205
+ /** The visitor's language candidates, most preferred first: `setLocale()` / `locale` option, traits, device. */
3206
+ locales() {
3207
+ return visitorLocales(this.locale, this.user?.traits, [this.bridge.device.locale]);
3208
+ }
3209
+ defaultLocale() {
3210
+ return this.config?.settings?.localization?.defaultLocale || "en";
3211
+ }
3212
+ /** The guide in the visitor's language (remembered so its events carry `locale`). */
3213
+ localize(guide) {
3214
+ const localized = localizeGuide(guide, this.locales(), this.defaultLocale());
3215
+ this.guideLocales.set(guide.id, localized.locale);
3216
+ return localized;
3217
+ }
3218
+ /**
3219
+ * Sets the language for guides and the resource center (`null` = back to the user's traits and
3220
+ * the device language). Guides already on screen keep their language; the resource center re-renders.
3221
+ */
3222
+ setLocale(locale) {
3223
+ const next = locale ? normalizeLocale(locale) || null : null;
3224
+ if (next === this.locale) return;
3225
+ this.locale = next;
3226
+ this.notify();
3227
+ }
2687
3228
  triggerFires(guide, reason, detail) {
2688
3229
  const t = guide.config.trigger;
2689
3230
  switch (t.type) {
@@ -2719,7 +3260,7 @@ var GuidelyClient = class {
2719
3260
  this.lastAudiences = current;
2720
3261
  }
2721
3262
  const candidates = [];
2722
- for (const guide of this.config.guides) {
3263
+ for (const guide of this.candidates()) {
2723
3264
  const t = guide.config.trigger;
2724
3265
  if (detail.endedGuide && detail.endedGuide.id === guide.id) continue;
2725
3266
  if (reason === "ready" || reason === "navigation") {
@@ -2735,6 +3276,10 @@ var GuidelyClient = class {
2735
3276
  if (t.type === "page_load" && t.delayMs > 0 && reason === "timer" && detail.guideId !== guide.id) continue;
2736
3277
  if (!this.triggerFires(guide, reason, detail)) continue;
2737
3278
  if (!this.isEligible(guide, ctx, url)) continue;
3279
+ if (!this.throttleAllows(guide)) {
3280
+ this.log("throttled", guide.key);
3281
+ continue;
3282
+ }
2738
3283
  candidates.push(guide);
2739
3284
  }
2740
3285
  if (reason === "after_guide" || reason === "navigation") this.drainQueue(url, ctx);
@@ -2780,6 +3325,12 @@ var GuidelyClient = class {
2780
3325
  }
2781
3326
  tryShow(guide, url, manual) {
2782
3327
  if (this.destroyed) return false;
3328
+ if (!manual && !this.throttleAllows(guide)) return false;
3329
+ if (this.holdoutFor(guide)) {
3330
+ if (this.hasExclusive() && !manual) return false;
3331
+ this.recordImpression(guide, manual);
3332
+ return true;
3333
+ }
2783
3334
  if (!isExclusiveGuide(guide)) {
2784
3335
  if (this.flows.has(guide.id) || this.checklists.has(guide.id)) return false;
2785
3336
  this.show(guide, manual);
@@ -2821,14 +3372,18 @@ var GuidelyClient = class {
2821
3372
  guideState(id) {
2822
3373
  return this.state.guides[id] ?? (this.state.guides[id] = { impressions: 0 });
2823
3374
  }
2824
- recordImpression(guide) {
3375
+ recordImpression(guide, manual = false) {
2825
3376
  const s = this.guideState(guide.id);
2826
3377
  s.impressions += 1;
2827
3378
  s.lastShownAt = nowIso();
2828
3379
  if (!this.session.shownGuides.includes(guide.id)) this.session.shownGuides.push(guide.id);
3380
+ this.recordLaunch(guide, manual);
2829
3381
  this.persistState();
2830
3382
  this.persistSession();
2831
- this.syncGuideState(guide.id, { impression: true });
3383
+ const holdout = this.holdoutFor(guide);
3384
+ const experiment = holdout ? { id: holdout.experimentId, variantId: holdout.variantId } : guide.experiment;
3385
+ if (experiment) this.enqueue("experiment_viewed", { properties: { guideExperimentId: experiment.id, variantId: experiment.variantId, ...holdout ? {} : { guideId: guide.id } } });
3386
+ if (!holdout) this.syncGuideState(guide.id, { impression: true });
2832
3387
  }
2833
3388
  recordEnd(guide, reason) {
2834
3389
  const s = this.guideState(guide.id);
@@ -2856,7 +3411,8 @@ var GuidelyClient = class {
2856
3411
  this.reportError(err);
2857
3412
  }
2858
3413
  }
2859
- showFlow(guide, manual, stepId) {
3414
+ showFlow(raw, manual, stepId) {
3415
+ const guide = this.localize(raw);
2860
3416
  const exclusive = isExclusiveGuide(guide);
2861
3417
  const flow = { guide, manual, view: null, controller: null };
2862
3418
  let renders = 0;
@@ -2871,7 +3427,7 @@ var GuidelyClient = class {
2871
3427
  const step = e.stepId ? guide.config.steps.find((s) => s.id === e.stepId) : void 0;
2872
3428
  this.enqueue(e.name, { guideId: guide.id, guideVersion: guide.version, stepId: e.stepId, stepIndex: e.stepIndex, properties: e.properties });
2873
3429
  if (e.name === "guide_started") {
2874
- this.recordImpression(guide);
3430
+ this.recordImpression(guide, manual);
2875
3431
  this.emitter.emit("guide:start", guide);
2876
3432
  } else if (e.name === "step_viewed" && step) {
2877
3433
  this.emitter.emit("guide:step", { guide, stepId: step.id, stepIndex: e.stepIndex ?? 0 });
@@ -2918,21 +3474,24 @@ var GuidelyClient = class {
2918
3474
  if (this.flows.get(guide.id) === flow) this.flows.delete(guide.id);
2919
3475
  }
2920
3476
  }
2921
- showChecklist(guide, open) {
3477
+ showChecklist(raw, open) {
3478
+ if (!raw.config.checklist || this.checklists.has(raw.id)) return;
3479
+ const guide = this.localize(raw);
2922
3480
  const config = guide.config.checklist;
2923
- if (!config || this.checklists.has(guide.id)) return;
3481
+ if (!config) return;
2924
3482
  const progress = this.getChecklistProgress(guide.id);
2925
3483
  const first = !this.state.guides[guide.id]?.impressions;
2926
- this.checklists.set(guide.id, { open: open ?? (config.autoOpen && first && !progress.completedAt) });
3484
+ this.checklists.set(guide.id, { guide, open: open ?? (config.autoOpen && first && !progress.completedAt) });
2927
3485
  if (first) this.recordImpression(guide);
2928
3486
  if (this.checklists.get(guide.id)?.open) this.enqueue("guide_viewed", { guideId: guide.id, guideVersion: guide.version });
2929
3487
  this.notify();
2930
3488
  }
2931
- showSurvey(guide, manual) {
2932
- if (!guide.config.survey) return;
3489
+ showSurvey(raw, manual) {
3490
+ if (!raw.config.survey) return;
3491
+ const guide = this.localize(raw);
2933
3492
  this.activeSurvey = { guide, manual };
2934
3493
  this.surveyView = { key: `${guide.id}:${randomId(6)}`, guide, submitted: false };
2935
- this.recordImpression(guide);
3494
+ this.recordImpression(guide, manual);
2936
3495
  this.enqueue("survey_displayed", { guideId: guide.id, guideVersion: guide.version });
2937
3496
  this.emitter.emit("guide:start", guide);
2938
3497
  this.notify();
@@ -2992,10 +3551,7 @@ var GuidelyClient = class {
2992
3551
  const steps = [];
2993
3552
  for (const f of [...this.flows.values(), ...this.active ? [this.active] : []]) if (f.view) steps.push({ ...f.view, expanded: this.expanded.has(f.guide.id) });
2994
3553
  const checklists = [];
2995
- this.checklists.forEach((w, id) => {
2996
- const guide = this.findGuide(id);
2997
- if (guide) checklists.push({ guide, progress: this.getChecklistProgress(id), open: w.open });
2998
- });
3554
+ this.checklists.forEach((w, id) => checklists.push({ guide: w.guide, progress: this.getChecklistProgress(id), open: w.open }));
2999
3555
  this.ui = { steps, checklists, survey: this.surveyView, resourceCenterOpen: this.resourceCenterOpen, theme: this.themeFor(), messenger: !!this.messenger };
3000
3556
  for (const cb of Array.from(this.uiListeners)) {
3001
3557
  try {
@@ -3080,10 +3636,58 @@ var GuidelyClient = class {
3080
3636
  getAudienceIds() {
3081
3637
  return this.state.audienceIds;
3082
3638
  }
3639
+ /** The published resource center in the visitor's language, or null when it is off. */
3083
3640
  getResourceCenter() {
3084
- const rc = this.config?.resourceCenter;
3641
+ const rc = this.localizedResourceCenter();
3085
3642
  return rc && rc.enabled ? rc : null;
3086
3643
  }
3644
+ /** The resource center in the visitor's language (cached until the config or language changes). */
3645
+ localizedResourceCenter() {
3646
+ const rc = this.config?.resourceCenter;
3647
+ if (!rc) return null;
3648
+ const locales = this.locales();
3649
+ const key = `${locales.join(",")}|${this.defaultLocale()}`;
3650
+ if (this.localizedRc?.source !== rc || this.localizedRc.locales !== key) this.localizedRc = { source: rc, locales: key, value: localizeResourceCenter(rc, locales, this.defaultLocale()) };
3651
+ return this.localizedRc.value;
3652
+ }
3653
+ /** Evaluates targeting rules (e.g. a resource-center section's) for the current visitor and screen. */
3654
+ matchesTargeting(rules) {
3655
+ if (!rules) return true;
3656
+ try {
3657
+ return evaluateRules(rules, this.getContext());
3658
+ } catch (err) {
3659
+ this.reportError(err);
3660
+ return false;
3661
+ }
3662
+ }
3663
+ /** Resource-center sections this visitor sees, in order (disabled and non-matching ones left out). */
3664
+ getResourceCenterModules() {
3665
+ const rc = this.getResourceCenter();
3666
+ if (!rc) return [];
3667
+ return orderedModules(rc.modules).filter((m) => m.enabled && (!("targeting" in m) || this.matchesTargeting(m.targeting))).map((m) => m.id);
3668
+ }
3669
+ /**
3670
+ * Sections of the messenger's Help tab, in order: the resource center's modules without "What's
3671
+ * new" (the messenger has its own News tab); every section when no resource center is published.
3672
+ */
3673
+ getHelpModules() {
3674
+ const modules = this.getResourceCenter() ? this.getResourceCenterModules() : [...RC_MODULE_IDS];
3675
+ return modules.filter((id) => id !== "announcements");
3676
+ }
3677
+ /** "What's new" entries this visitor sees (newest first); empty when the section is off. */
3678
+ getAnnouncements() {
3679
+ const rc = this.getResourceCenter();
3680
+ if (!rc || !this.getResourceCenterModules().includes("announcements")) return [];
3681
+ return visibleAnnouncements(rc.announcements, this.state.audienceIds);
3682
+ }
3683
+ /** "What's new" entries published since the visitor last opened the resource center. */
3684
+ getUnreadAnnouncements() {
3685
+ return unreadAnnouncements(this.getAnnouncements(), this.state.announcementsSeenAt);
3686
+ }
3687
+ /** Whether an entry is new to the visitor in the resource center that is open now. */
3688
+ isAnnouncementNew(a) {
3689
+ return unreadAnnouncements([a], this.announcementsSeenAtOpen) > 0;
3690
+ }
3087
3691
  async searchResourceCenter(q) {
3088
3692
  try {
3089
3693
  const res = await this.api.get("/resource-center/search", { params: { q } });
@@ -3092,6 +3696,11 @@ var GuidelyClient = class {
3092
3696
  return [];
3093
3697
  }
3094
3698
  }
3699
+ /** The language of the resource center as shown, when it isn't the default (sent with article fetches). */
3700
+ articleLocale() {
3701
+ const locale = this.localizedResourceCenter()?.locale;
3702
+ return locale && locale.toLowerCase() !== this.defaultLocale().toLowerCase() ? locale : void 0;
3703
+ }
3095
3704
  articleViewed(article) {
3096
3705
  this.enqueue("article_viewed", { properties: { articleId: article.id, slug: article.slug } });
3097
3706
  }
@@ -3226,6 +3835,11 @@ var GuidelyClient = class {
3226
3835
  }
3227
3836
  if (this.resourceCenterOpen) return;
3228
3837
  this.resourceCenterOpen = true;
3838
+ this.announcementsSeenAtOpen = this.state.announcementsSeenAt ?? null;
3839
+ if (this.getAnnouncements().length) {
3840
+ this.state.announcementsSeenAt = nowIso();
3841
+ this.persistState();
3842
+ }
3229
3843
  this.enqueue("resource_center_opened");
3230
3844
  this.emitter.emit("resource-center:open");
3231
3845
  this.notify();
@@ -3261,7 +3875,8 @@ var GuidelyClient = class {
3261
3875
  onMessage: (message, conversation) => this.emitter.emit("conversation:message", { message, conversation }),
3262
3876
  onStartGuide: (key) => this.setTimer(() => this.startGuide(key), 0),
3263
3877
  onError: (err) => this.log("messenger", err instanceof Error ? err.message : err),
3264
- health: this.api.health
3878
+ health: this.api.health,
3879
+ locale: () => this.articleLocale()
3265
3880
  });
3266
3881
  this.resourceCenterOpen = false;
3267
3882
  }
@@ -3395,7 +4010,191 @@ var GuidelyClient = class {
3395
4010
  }
3396
4011
  };
3397
4012
  function forPlatform(config, platform) {
3398
- return config.guides.every((g) => supportsPlatform(g.config, platform)) ? config : { ...config, guides: config.guides.filter((g) => supportsPlatform(g.config, platform)) };
4013
+ const keep = (g) => supportsPlatform(g.config, platform);
4014
+ const holdouts = config.guideHoldouts?.filter((h) => keep(h.guide)).map((h) => ({ ...h, guide: withoutCode(h.guide) }));
4015
+ return { ...config, guides: config.guides.filter(keep).map(withoutCode), ...holdouts ? { guideHoldouts: holdouts } : {} };
4016
+ }
4017
+ function withoutCode(guide) {
4018
+ if (!guide.config.steps.some((s) => s.code)) return guide;
4019
+ return { ...guide, config: { ...guide.config, steps: guide.config.steps.map(({ code: _code, ...step }) => step) } };
4020
+ }
4021
+
4022
+ // src/ui/Autocapture.tsx
4023
+ import { useMemo as useMemo5, useRef as useRef5 } from "react";
4024
+ import { View as View10 } from "react-native";
4025
+
4026
+ // src/core/viewTree.ts
4027
+ var HOST_TEXT = 6;
4028
+ var MAX_ANCESTORS = 500;
4029
+ var MAX_TEXT_NODES = 300;
4030
+ var INTERACTIVE_ROLES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem", "checkbox", "switch", "radio", "togglebutton", "imagebutton", "option", "combobox", "adjustable"]);
4031
+ var TEXT_INPUT_HOST = /TextInput/i;
4032
+ var NOISE = /^(Anonymous|Unknown|ForwardRef|Memo|Context|Provider|Consumer|Fragment|StrictMode|Suspense|Profiler)$|Context$|Provider$|Consumer$/;
4033
+ var isFiber = (v) => !!v && typeof v === "object" && "return" in v && "memoizedProps" in v;
4034
+ function fiberFromEvent(event) {
4035
+ try {
4036
+ const e = event;
4037
+ if (isFiber(e?._targetInst)) return e._targetInst;
4038
+ const target = e?.target;
4039
+ if (!target || typeof target !== "object") return null;
4040
+ for (const key of ["__internalInstanceHandle", "_internalInstanceHandle", "_internalFiberInstanceHandleDEV"]) if (isFiber(target[key])) return target[key];
4041
+ for (const sym of Object.getOwnPropertySymbols(target)) if (sym.description === "internalInstanceHandle" && isFiber(target[sym])) return target[sym];
4042
+ } catch {
4043
+ }
4044
+ return null;
4045
+ }
4046
+ function componentName(type, depth = 0) {
4047
+ if (!type || depth > 3) return null;
4048
+ if (typeof type === "function") {
4049
+ const f = type;
4050
+ const n = typeof f.displayName === "string" && f.displayName ? f.displayName : typeof f.name === "string" ? f.name : "";
4051
+ return n || null;
4052
+ }
4053
+ if (typeof type === "object") {
4054
+ const o = type;
4055
+ if (typeof o.displayName === "string" && o.displayName) return o.displayName;
4056
+ return componentName(o.render, depth + 1) ?? componentName(o.type, depth + 1);
4057
+ }
4058
+ return null;
4059
+ }
4060
+ var isHost = (f) => typeof f.type === "string";
4061
+ var isTextInput = (f) => typeof f.type === "string" ? TEXT_INPUT_HOST.test(f.type) : componentName(f.type) === "TextInput";
4062
+ function isInteractive(props) {
4063
+ if (!props || typeof props !== "object") return null;
4064
+ if (typeof props.onPress === "function" || typeof props.onLongPress === "function") return "press";
4065
+ const role = props.accessibilityRole ?? props.role;
4066
+ return typeof role === "string" && INTERACTIVE_ROLES.has(role) ? "role" : null;
4067
+ }
4068
+ function firstHost(f) {
4069
+ let cur = f;
4070
+ for (let i = 0; cur && i < 30; i++, cur = cur.child) if (isHost(cur)) return cur;
4071
+ return null;
4072
+ }
4073
+ function idSelector(f) {
4074
+ const host = firstHost(f);
4075
+ const sources = [f.memoizedProps, host?.memoizedProps];
4076
+ for (const key of ["nativeID", "testID"]) {
4077
+ for (const p of sources) {
4078
+ const v = p && typeof p === "object" ? p[key] : void 0;
4079
+ if (typeof v === "string" && v) return attrSelector(key, v);
4080
+ }
4081
+ }
4082
+ return null;
4083
+ }
4084
+ function pathSelector(f) {
4085
+ const parts = [];
4086
+ let cur = f;
4087
+ for (let i = 0; cur && parts.length < 4 && i < MAX_ANCESTORS; i++, cur = cur.return) {
4088
+ if (isHost(cur)) continue;
4089
+ const name = componentName(cur.type);
4090
+ if (!name || NOISE.test(name)) continue;
4091
+ let same = 0;
4092
+ let index = 0;
4093
+ for (let s = cur.return?.child, n = 0; s && n < 500; s = s.sibling, n++) {
4094
+ if (s.type !== cur.type) continue;
4095
+ same += 1;
4096
+ if (s === cur) index = same;
4097
+ }
4098
+ parts.unshift(same > 1 && index ? `${name}:nth-of-type(${index})` : name);
4099
+ }
4100
+ return parts.join(" > ").slice(0, 300) || "View";
4101
+ }
4102
+ function subtreeText(f, opts) {
4103
+ const out = [];
4104
+ let visited = 0;
4105
+ let masked = false;
4106
+ const walk = (node) => {
4107
+ for (let n = node; n && visited < MAX_TEXT_NODES && !masked; n = n.sibling) {
4108
+ visited += 1;
4109
+ if (opts.isMask?.(n.type)) {
4110
+ masked = true;
4111
+ return;
4112
+ }
4113
+ if (isTextInput(n)) continue;
4114
+ if (n.tag === HOST_TEXT && (typeof n.memoizedProps === "string" || typeof n.memoizedProps === "number")) out.push(String(n.memoizedProps));
4115
+ else walk(n.child);
4116
+ }
4117
+ };
4118
+ if (opts.isMask?.(f.type)) return { text: "", masked: true };
4119
+ walk(f.child);
4120
+ return { text: out.join(" "), masked };
4121
+ }
4122
+ function measureHost(host) {
4123
+ return new Promise((resolve) => {
4124
+ let settled = false;
4125
+ const done = (r) => {
4126
+ if (settled) return;
4127
+ settled = true;
4128
+ clearTimeout(timer);
4129
+ resolve(r);
4130
+ };
4131
+ const timer = setTimeout(() => done(null), 500);
4132
+ const cb = (x, y, width, height) => done(Number.isFinite(x) && Number.isFinite(y) && width > 0 && height > 0 ? { x, y, width, height } : null);
4133
+ try {
4134
+ const sn = host.stateNode;
4135
+ const pub = sn?.canonical?.publicInstance ?? sn;
4136
+ if (pub && typeof pub.measureInWindow === "function") return pub.measureInWindow(cb);
4137
+ const ui = globalThis.nativeFabricUIManager;
4138
+ if (sn?.node && typeof ui?.measureInWindow === "function") return ui.measureInWindow(sn.node, cb);
4139
+ } catch {
4140
+ }
4141
+ done(null);
4142
+ });
4143
+ }
4144
+ function describe(f, masked, opts, withText) {
4145
+ const props = f.memoizedProps && typeof f.memoizedProps === "object" ? f.memoizedProps : {};
4146
+ const host = firstHost(f);
4147
+ const el = { selector: idSelector(f) ?? pathSelector(f), tag: componentName(f.type) ?? "View" };
4148
+ if (withText && !masked) {
4149
+ const sub = subtreeText(f, opts);
4150
+ if (!sub.masked) {
4151
+ const hostProps = host?.memoizedProps && typeof host.memoizedProps === "object" ? host.memoizedProps : {};
4152
+ const label = props.accessibilityLabel ?? props["aria-label"] ?? hostProps.accessibilityLabel ?? hostProps["aria-label"];
4153
+ const text = cleanText(sub.text) ?? (typeof label === "string" ? cleanText(label) : void 0);
4154
+ if (text) el.text = text;
4155
+ }
4156
+ }
4157
+ if (host) el.measure = () => measureHost(host);
4158
+ return el;
4159
+ }
4160
+ function resolveFiber(target, opts = {}) {
4161
+ const none = { ignored: false, element: null, hit: null };
4162
+ if (!target) return none;
4163
+ try {
4164
+ let press = null;
4165
+ let role = null;
4166
+ let hitFiber = null;
4167
+ let stopped = false;
4168
+ let masked = false;
4169
+ for (let f = target, i = 0; f && i < MAX_ANCESTORS; f = f.return, i++) {
4170
+ if (opts.isIgnored?.(f.type)) return { ignored: true, element: null, hit: null };
4171
+ if (opts.isMask?.(f.type)) masked = true;
4172
+ if (stopped) continue;
4173
+ if (isTextInput(f)) {
4174
+ stopped = true;
4175
+ continue;
4176
+ }
4177
+ if (isHost(f)) continue;
4178
+ const name = componentName(f.type);
4179
+ if (!hitFiber && name && !NOISE.test(name)) hitFiber = f;
4180
+ const kind = isInteractive(f.memoizedProps);
4181
+ if (kind === "press") {
4182
+ press = f;
4183
+ stopped = true;
4184
+ } else if (kind === "role" && !role) role = f;
4185
+ }
4186
+ const element = press ?? role;
4187
+ return {
4188
+ ignored: false,
4189
+ element: element ? describe(element, masked, opts, true) : null,
4190
+ hit: hitFiber ? describe(hitFiber, masked, opts, componentName(hitFiber.type) === "Text") : null
4191
+ };
4192
+ } catch {
4193
+ return none;
4194
+ }
4195
+ }
4196
+ function resolveTapTarget(event, opts = {}) {
4197
+ return resolveFiber(fiberFromEvent(event), opts);
3399
4198
  }
3400
4199
 
3401
4200
  // src/ui/Overlay.tsx
@@ -3919,9 +4718,9 @@ var styles3 = StyleSheet3.create({
3919
4718
  });
3920
4719
 
3921
4720
  // src/ui/Messenger.tsx
3922
- import { useCallback, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2, useState as useState3, useSyncExternalStore as useSyncExternalStore2 } from "react";
4721
+ import { Fragment as Fragment2, useCallback, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2, useState as useState3, useSyncExternalStore as useSyncExternalStore2 } from "react";
3923
4722
  import { ActivityIndicator, Keyboard, Modal as Modal2, Platform as Platform2, Pressable as Pressable4, ScrollView as ScrollView3, StyleSheet as StyleSheet4, Text as Text4, TextInput, View as View4 } from "react-native";
3924
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
4723
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
3925
4724
  function useMessenger(store) {
3926
4725
  return useSyncExternalStore2(
3927
4726
  useCallback((cb) => store.subscribe(cb), [store]),
@@ -4155,25 +4954,41 @@ function Help({ store, client, palette: p }) {
4155
4954
  ] }, title);
4156
4955
  const audiences = client.getAudienceIds();
4157
4956
  const sections = resourceCenterSections(articles?.collections ?? [], articles?.articles ?? [], audiences);
4158
- const listed = results?.filter((a) => isArticleListed(articles?.collections ?? [], a, audiences));
4957
+ const modules = client.getHelpModules();
4958
+ const searchOn = modules.includes("search");
4959
+ const listed = searchOn ? results?.filter((a) => isArticleListed(articles?.collections ?? [], a, audiences)) : void 0;
4960
+ const checklists = (rc?.checklistGuideIds ?? []).map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g && g.kind === "checklist");
4961
+ const renderModule = (id) => {
4962
+ switch (id) {
4963
+ case "guides":
4964
+ return recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u25B6"))) : null;
4965
+ case "checklists":
4966
+ return checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u2611"))) : null;
4967
+ case "articles":
4968
+ return /* @__PURE__ */ jsxs3(Fragment3, { children: [
4969
+ !articles ? /* @__PURE__ */ jsx5(ActivityIndicator, { color: p.muted }) : null,
4970
+ sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => open(a)))))
4971
+ ] });
4972
+ case "faqs":
4973
+ return rc?.faqs.length ? section(
4974
+ "FAQ",
4975
+ rc.faqs.map((f) => /* @__PURE__ */ jsxs3(View4, { style: [styles4.row, styles4.faq, { borderColor: p.border }], children: [
4976
+ /* @__PURE__ */ jsxs3(Pressable4, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles4.faqHead, children: [
4977
+ /* @__PURE__ */ jsx5(Text4, { style: [styles4.rowTitle, styles4.flex, { color: p.text }], children: f.question }),
4978
+ /* @__PURE__ */ jsx5(Text4, { style: [styles4.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
4979
+ ] }),
4980
+ faq === f.id ? /* @__PURE__ */ jsx5(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
4981
+ ] }, f.id))
4982
+ ) : null;
4983
+ case "links":
4984
+ return rc?.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null;
4985
+ default:
4986
+ return null;
4987
+ }
4988
+ };
4159
4989
  return /* @__PURE__ */ jsxs3(ScrollView3, { contentContainerStyle: styles4.content, keyboardShouldPersistTaps: "handled", children: [
4160
- /* @__PURE__ */ jsx5(TextInput, { value: q, onChangeText: setQ, placeholder: "Search articles", placeholderTextColor: p.muted, accessibilityLabel: "Search help articles", returnKeyType: "search", style: [styles4.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }] }),
4161
- listed ? section(`Results for \u201C${q.trim()}\u201D`, listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => open(a))) : /* @__PURE__ */ jsx5(Text4, { style: [styles4.rowSub, { color: p.muted }], children: "No articles found." })) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
4162
- recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u25B6"))) : null,
4163
- !articles ? /* @__PURE__ */ jsx5(ActivityIndicator, { color: p.muted }) : null,
4164
- sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => open(a))))),
4165
- rc?.faqs.length ? section(
4166
- "FAQ",
4167
- rc.faqs.map((f) => /* @__PURE__ */ jsxs3(View4, { style: [styles4.row, styles4.faq, { borderColor: p.border }], children: [
4168
- /* @__PURE__ */ jsxs3(Pressable4, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles4.faqHead, children: [
4169
- /* @__PURE__ */ jsx5(Text4, { style: [styles4.rowTitle, styles4.flex, { color: p.text }], children: f.question }),
4170
- /* @__PURE__ */ jsx5(Text4, { style: [styles4.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
4171
- ] }),
4172
- faq === f.id ? /* @__PURE__ */ jsx5(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
4173
- ] }, f.id))
4174
- ) : null,
4175
- rc?.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null
4176
- ] })
4990
+ searchOn ? /* @__PURE__ */ jsx5(TextInput, { value: q, onChangeText: setQ, placeholder: "Search articles", placeholderTextColor: p.muted, accessibilityLabel: "Search help articles", returnKeyType: "search", style: [styles4.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }] }) : null,
4991
+ listed ? section(`Results for \u201C${q.trim()}\u201D`, listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => open(a))) : /* @__PURE__ */ jsx5(Text4, { style: [styles4.rowSub, { color: p.muted }], children: "No articles found." })) : /* @__PURE__ */ jsx5(Fragment3, { children: modules.filter((id) => id !== "search").map((id) => /* @__PURE__ */ jsx5(Fragment2, { children: renderModule(id) }, id)) })
4177
4992
  ] });
4178
4993
  }
4179
4994
  function Article({ article, palette: p, client }) {
@@ -4240,9 +5055,9 @@ var styles4 = StyleSheet4.create({
4240
5055
  });
4241
5056
 
4242
5057
  // src/ui/ResourceCenter.tsx
4243
- import { useEffect as useEffect4, useMemo as useMemo4, useState as useState4 } from "react";
5058
+ import { Fragment as Fragment4, useEffect as useEffect4, useMemo as useMemo4, useState as useState4 } from "react";
4244
5059
  import { Modal as Modal3, Platform as Platform3, Pressable as Pressable5, ScrollView as ScrollView4, StyleSheet as StyleSheet5, Text as Text5, TextInput as TextInput2, View as View5 } from "react-native";
4245
- import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
5060
+ import { Fragment as Fragment5, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
4246
5061
  function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4247
5062
  const rc = client.getResourceCenter();
4248
5063
  const [query, setQuery] = useState4("");
@@ -4276,6 +5091,8 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4276
5091
  const checklists = useMemo4(() => (rc?.checklistGuideIds ?? []).map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g && g.kind === "checklist"), [rc, guides]);
4277
5092
  if (!rc) return null;
4278
5093
  const close = () => client.closeResourceCenter();
5094
+ const modules = client.getResourceCenterModules();
5095
+ const announcements = client.getAnnouncements();
4279
5096
  const openArticle = (a) => {
4280
5097
  if (a.externalUrl) return client.navigate(a.externalUrl);
4281
5098
  client.articleViewed(a);
@@ -4294,7 +5111,51 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4294
5111
  ] }, title);
4295
5112
  const audiences = client.getAudienceIds();
4296
5113
  const sections = resourceCenterSections(rc.collections, rc.articles, audiences);
4297
- const listed = results?.filter((a) => isArticleListed(rc.collections, a, audiences));
5114
+ const listed = modules.includes("search") ? results?.filter((a) => isArticleListed(rc.collections, a, audiences)) : void 0;
5115
+ const link = (label, onPress) => /* @__PURE__ */ jsx6(Pressable5, { accessibilityRole: "button", accessibilityLabel: label, onPress, hitSlop: 8, children: /* @__PURE__ */ jsx6(Text5, { style: [styles5.link, { color: p.primary }], children: label }) }, label);
5116
+ const announcement2 = (a) => {
5117
+ const linked = a.articleId ? rc.articles.find((x) => x.id === a.articleId) : void 0;
5118
+ const isNew = client.isAnnouncementNew(a);
5119
+ return /* @__PURE__ */ jsxs4(View5, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
5120
+ /* @__PURE__ */ jsxs4(View5, { style: styles5.faqHead, children: [
5121
+ /* @__PURE__ */ jsx6(Text5, { accessibilityRole: "header", style: [styles5.rowTitle, styles5.flex, { color: p.text, fontFamily: p.fontFamily }], children: a.title }),
5122
+ isNew ? /* @__PURE__ */ jsx6(Text5, { style: [styles5.badge, { color: p.onPrimary, backgroundColor: p.primary }], children: "NEW" }) : null
5123
+ ] }),
5124
+ a.body ? /* @__PURE__ */ jsx6(RichText, { html: a.body, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null,
5125
+ a.guideId || linked || a.url ? /* @__PURE__ */ jsxs4(View5, { style: styles5.links, children: [
5126
+ a.guideId ? link("Show me", () => (close(), client.startGuide(a.guideId))) : null,
5127
+ linked ? link("Read more", () => openArticle(linked)) : null,
5128
+ a.url ? link(a.linkLabel || "Learn more", () => client.navigate(a.url)) : null
5129
+ ] }) : null
5130
+ ] }, a.id);
5131
+ };
5132
+ const renderModule = (id) => {
5133
+ switch (id) {
5134
+ case "announcements":
5135
+ return announcements.length ? section("What's new", announcements.map(announcement2)) : null;
5136
+ case "guides":
5137
+ return recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => (close(), client.startGuide(g.id)), "\u25B6"))) : null;
5138
+ case "checklists":
5139
+ return checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => (close(), client.openChecklist(g.id)), "\u2611"))) : null;
5140
+ case "articles":
5141
+ return sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => openArticle(a)))));
5142
+ case "faqs":
5143
+ return rc.faqs.length ? section(
5144
+ "FAQ",
5145
+ rc.faqs.map((f) => /* @__PURE__ */ jsxs4(View5, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
5146
+ /* @__PURE__ */ jsxs4(Pressable5, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles5.faqHead, children: [
5147
+ /* @__PURE__ */ jsx6(Text5, { style: [styles5.rowTitle, styles5.flex, { color: p.text }], children: f.question }),
5148
+ /* @__PURE__ */ jsx6(Text5, { style: [styles5.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
5149
+ ] }),
5150
+ faq === f.id ? /* @__PURE__ */ jsx6(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
5151
+ ] }, f.id))
5152
+ ) : null;
5153
+ case "links":
5154
+ return rc.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null;
5155
+ default:
5156
+ return null;
5157
+ }
5158
+ };
4298
5159
  return /* @__PURE__ */ jsx6(Modal3, { visible: open, animationType: "slide", presentationStyle: Platform3.OS === "ios" ? "pageSheet" : "fullScreen", onRequestClose: () => article ? setArticle(null) : close(), children: /* @__PURE__ */ jsxs4(View5, { style: [styles5.sheet, { backgroundColor: p.background, paddingTop: Platform3.OS === "ios" ? 8 : topInset }], children: [
4299
5160
  /* @__PURE__ */ jsxs4(View5, { style: [styles5.header, { borderColor: p.border }], children: [
4300
5161
  article ? /* @__PURE__ */ jsx6(Pressable5, { accessibilityRole: "button", accessibilityLabel: "Back", onPress: () => setArticle(null), hitSlop: 10, children: /* @__PURE__ */ jsx6(Text5, { style: [styles5.back, { color: p.primary }], children: "\u2039 Back" }) }) : null,
@@ -4305,7 +5166,7 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4305
5166
  /* @__PURE__ */ jsx6(Text5, { accessibilityRole: "header", style: [styles5.articleTitle, { color: p.text, fontFamily: p.fontFamily }], children: article.title }),
4306
5167
  /* @__PURE__ */ jsx6(RichText, { html: article.body, palette: p, onLink: (u) => client.navigate(u) })
4307
5168
  ] }) : /* @__PURE__ */ jsxs4(ScrollView4, { contentContainerStyle: [styles5.content, { paddingBottom: bottomInset + 24 }], keyboardShouldPersistTaps: "handled", children: [
4308
- /* @__PURE__ */ jsx6(
5169
+ modules.includes("search") ? /* @__PURE__ */ jsx6(
4309
5170
  TextInput2,
4310
5171
  {
4311
5172
  value: query,
@@ -4317,26 +5178,11 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4317
5178
  returnKeyType: "search",
4318
5179
  style: [styles5.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }]
4319
5180
  }
4320
- ),
5181
+ ) : null,
4321
5182
  listed ? section(
4322
5183
  `Results for \u201C${query.trim()}\u201D`,
4323
5184
  listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => openArticle(a))) : /* @__PURE__ */ jsx6(Text5, { style: [styles5.rowSub, { color: p.muted }], children: "No articles found." })
4324
- ) : /* @__PURE__ */ jsxs4(Fragment3, { children: [
4325
- recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => (close(), client.startGuide(g.id)), "\u25B6"))) : null,
4326
- checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => (close(), client.openChecklist(g.id)), "\u2611"))) : null,
4327
- sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => openArticle(a))))),
4328
- rc.faqs.length ? section(
4329
- "FAQ",
4330
- rc.faqs.map((f) => /* @__PURE__ */ jsxs4(View5, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
4331
- /* @__PURE__ */ jsxs4(Pressable5, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles5.faqHead, children: [
4332
- /* @__PURE__ */ jsx6(Text5, { style: [styles5.rowTitle, styles5.flex, { color: p.text }], children: f.question }),
4333
- /* @__PURE__ */ jsx6(Text5, { style: [styles5.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
4334
- ] }),
4335
- faq === f.id ? /* @__PURE__ */ jsx6(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
4336
- ] }, f.id))
4337
- ) : null,
4338
- rc.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null
4339
- ] })
5185
+ ) : /* @__PURE__ */ jsx6(Fragment5, { children: modules.filter((id) => id !== "search").map((id) => /* @__PURE__ */ jsx6(Fragment4, { children: renderModule(id) }, id)) })
4340
5186
  ] })
4341
5187
  ] }) });
4342
5188
  }
@@ -4356,7 +5202,10 @@ var styles5 = StyleSheet5.create({
4356
5202
  chev: { fontSize: 18 },
4357
5203
  faq: { flexDirection: "column", alignItems: "stretch" },
4358
5204
  faqHead: { flexDirection: "row", alignItems: "center", gap: 10 },
4359
- articleTitle: { fontSize: 22, fontWeight: "700" }
5205
+ articleTitle: { fontSize: 22, fontWeight: "700" },
5206
+ badge: { fontSize: 10, fontWeight: "800", letterSpacing: 0.5, paddingHorizontal: 6, paddingVertical: 2, borderRadius: 6, overflow: "hidden" },
5207
+ links: { flexDirection: "row", flexWrap: "wrap", gap: 16, marginTop: 4 },
5208
+ link: { fontSize: 14, fontWeight: "600" }
4360
5209
  });
4361
5210
 
4362
5211
  // src/ui/StepLayer.tsx
@@ -4410,7 +5259,19 @@ function StepCard({ view, client, palette: p, width, style, compact }) {
4410
5259
  const showProgress = step.showProgress && view.total > 1;
4411
5260
  const onLink = (url) => client.navigate(url);
4412
5261
  return /* @__PURE__ */ jsx7(Surface, { palette: p, style: [{ width }, style], children: /* @__PURE__ */ jsxs5(View6, { accessible: false, accessibilityViewIsModal: view.asModal || step.type === "modal", style: compact ? styles6.compact : styles6.inner, children: [
4413
- step.imageUrl ? /* @__PURE__ */ jsx7(Image2, { source: { uri: step.imageUrl }, style: [styles6.image, { borderRadius: Math.max(0, p.radius - 4) }], resizeMode: "cover", accessibilityIgnoresInvertColors: true }) : null,
5262
+ step.imageUrl ? (
5263
+ // An empty description marks the image as decorative: screen readers skip it.
5264
+ /* @__PURE__ */ jsx7(
5265
+ Image2,
5266
+ {
5267
+ source: { uri: step.imageUrl },
5268
+ style: [styles6.image, { borderRadius: Math.max(0, p.radius - 4) }],
5269
+ resizeMode: "cover",
5270
+ accessibilityIgnoresInvertColors: true,
5271
+ ...step.imageAlt?.trim() ? { accessible: true, accessibilityRole: "image", accessibilityLabel: step.imageAlt.trim() } : { accessible: false, importantForAccessibility: "no" }
5272
+ }
5273
+ )
5274
+ ) : null,
4414
5275
  step.title ? /* @__PURE__ */ jsxs5(View6, { style: styles6.header, children: [
4415
5276
  /* @__PURE__ */ jsx7(Text6, { accessibilityRole: "header", style: [styles6.title, { color: p.text, fontFamily: p.fontFamily }], children: step.title }),
4416
5277
  step.showClose ? /* @__PURE__ */ jsx7(CloseButton, { palette: p, onPress: () => client.closeStep(guide.id), label: "Dismiss guide" }) : null
@@ -5008,18 +5869,22 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
5008
5869
  const basePalette = palette(ui.theme, scheme);
5009
5870
  const messenger = ui.messenger ? client.getMessenger() : null;
5010
5871
  const showRcLauncher = !messenger && !!client.options.showResourceCenterLauncher && !!client.getResourceCenter();
5872
+ const unread = showRcLauncher ? client.getUnreadAnnouncements() : 0;
5011
5873
  const messengerLeft = messenger?.getSnapshot().config.launcher.position === "bottom-left";
5012
5874
  const checklistOnSameSide = ui.checklists.some((c) => c.guide.config.checklist?.position === "bottom-left" === messengerLeft) && client.options.showChecklistLauncher !== false;
5013
5875
  return /* @__PURE__ */ jsxs8(View9, { ref, collapsable: false, pointerEvents: "box-none", style: StyleSheet9.absoluteFill, onLayout: measureOrigin, children: [
5014
5876
  ui.checklists.map((c) => /* @__PURE__ */ jsx10(Checklist, { view: c, client, palette: palette(client.themeFor(c.guide), scheme), geometry, showLauncher: client.options.showChecklistLauncher !== false, launcherBottom: launcherOffset }, c.guide.id)),
5015
- showRcLauncher ? /* @__PURE__ */ jsx10(
5877
+ showRcLauncher ? /* @__PURE__ */ jsxs8(
5016
5878
  Pressable8,
5017
5879
  {
5018
5880
  accessibilityRole: "button",
5019
- accessibilityLabel: client.getResourceCenter()?.launcher.label || "Help",
5881
+ accessibilityLabel: rcLauncherLabel(client),
5020
5882
  onPress: () => client.openResourceCenter(),
5021
5883
  style: [styles9.rc, client.getResourceCenter()?.launcher.position === "bottom-left" ? { left: insets.left + 16 } : { right: insets.right + 16 }, { bottom: insets.bottom + launcherOffset + (ui.checklists.length ? 56 : 0), backgroundColor: basePalette.primary }],
5022
- children: /* @__PURE__ */ jsx10(Text8, { style: [styles9.rcText, { color: basePalette.onPrimary }], children: "?" })
5884
+ children: [
5885
+ /* @__PURE__ */ jsx10(Text8, { style: [styles9.rcText, { color: basePalette.onPrimary }], children: "?" }),
5886
+ unread ? /* @__PURE__ */ jsx10(View9, { pointerEvents: "none", style: [styles9.rcBadge, { borderColor: basePalette.primary }], children: /* @__PURE__ */ jsx10(Text8, { style: styles9.rcBadgeText, children: unread > 9 ? "9+" : unread }) }) : null
5887
+ ]
5023
5888
  }
5024
5889
  ) : null,
5025
5890
  messenger && client.options.showMessengerLauncher !== false ? /* @__PURE__ */ jsx10(MessengerLauncher, { store: messenger, palette: basePalette, style: [messengerLeft ? { left: insets.left + 16 } : { right: insets.right + 16 }, { bottom: insets.bottom + launcherOffset + (checklistOnSameSide ? 56 : 0) }] }) : null,
@@ -5030,27 +5895,75 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
5030
5895
  }
5031
5896
  var styles9 = StyleSheet9.create({
5032
5897
  rc: { position: "absolute", width: 48, height: 48, borderRadius: 24, alignItems: "center", justifyContent: "center", elevation: 8, shadowColor: "#000", shadowOpacity: 0.2, shadowRadius: 10, shadowOffset: { width: 0, height: 4 } },
5033
- rcText: { fontSize: 22, fontWeight: "800" }
5898
+ rcText: { fontSize: 22, fontWeight: "800" },
5899
+ rcBadge: { position: "absolute", top: -4, right: -4, minWidth: 20, height: 20, paddingHorizontal: 5, borderRadius: 10, borderWidth: 2, backgroundColor: "#dc2626", alignItems: "center", justifyContent: "center" },
5900
+ rcBadgeText: { color: "#ffffff", fontSize: 11, fontWeight: "800" }
5034
5901
  });
5902
+ function rcLauncherLabel(client) {
5903
+ const label = client.getResourceCenter()?.launcher.label || "Help";
5904
+ const unread = client.getUnreadAnnouncements();
5905
+ return unread ? `${label} (${unread} new update${unread === 1 ? "" : "s"})` : label;
5906
+ }
5035
5907
 
5036
- // src/ui/Target.tsx
5037
- import { createContext as createContext2, useCallback as useCallback3, useContext as useContext2, useEffect as useEffect8, useRef as useRef5 } from "react";
5038
- import { View as View10 } from "react-native";
5908
+ // src/ui/Autocapture.tsx
5039
5909
  import { jsx as jsx11 } from "react/jsx-runtime";
5910
+ function GuidelyMask({ children, ...rest }) {
5911
+ return /* @__PURE__ */ jsx11(View10, { ...rest, children });
5912
+ }
5913
+ var RESOLVE = {
5914
+ isIgnored: (type) => type === GuidelyOverlay,
5915
+ isMask: (type) => type === GuidelyMask
5916
+ };
5917
+ function useTapObserver(client) {
5918
+ const clientRef = useRef5(client);
5919
+ clientRef.current = client;
5920
+ return useMemo5(() => {
5921
+ const tracker = new TapTracker();
5922
+ const guard = (fn) => (e) => {
5923
+ try {
5924
+ fn(e);
5925
+ } catch {
5926
+ tracker.cancel();
5927
+ }
5928
+ };
5929
+ return {
5930
+ onTouchStartCapture: guard((e) => {
5931
+ const n = e.nativeEvent;
5932
+ if (!clientRef.current?.isTapCaptureOn()) return tracker.cancel();
5933
+ tracker.begin(n.pageX, n.pageY, Date.now(), Array.isArray(n.touches) ? n.touches.length : 1, n.identifier);
5934
+ }),
5935
+ onTouchMoveCapture: guard((e) => tracker.move(e.nativeEvent.pageX, e.nativeEvent.pageY, e.nativeEvent.identifier)),
5936
+ onTouchEndCapture: guard((e) => {
5937
+ const n = e.nativeEvent;
5938
+ const tap = tracker.end(n.pageX, n.pageY, Date.now(), n.identifier);
5939
+ const c = clientRef.current;
5940
+ if (!tap || !c) return;
5941
+ const r = resolveTapTarget(e, RESOLVE);
5942
+ if (!r.ignored) c.recordTap({ x: tap.x, y: tap.y, t: Date.now(), element: r.element, hit: r.hit });
5943
+ }),
5944
+ onTouchCancelCapture: guard(() => tracker.cancel())
5945
+ };
5946
+ }, []);
5947
+ }
5948
+
5949
+ // src/ui/Target.tsx
5950
+ import { createContext as createContext2, useCallback as useCallback3, useContext as useContext2, useEffect as useEffect8, useRef as useRef6 } from "react";
5951
+ import { View as View11 } from "react-native";
5952
+ import { jsx as jsx12 } from "react/jsx-runtime";
5040
5953
  var GuidelyContext = createContext2(null);
5041
5954
  var GuidelyScreenContext = createContext2(null);
5042
5955
  function GuidelyScreenScope({ name, children }) {
5043
- return /* @__PURE__ */ jsx11(GuidelyScreenContext.Provider, { value: name, children });
5956
+ return /* @__PURE__ */ jsx12(GuidelyScreenContext.Provider, { value: name, children });
5044
5957
  }
5045
5958
  function guidelyScreenLayout({ route, children }) {
5046
- return /* @__PURE__ */ jsx11(GuidelyScreenScope, { name: route.name, children });
5959
+ return /* @__PURE__ */ jsx12(GuidelyScreenScope, { name: route.name, children });
5047
5960
  }
5048
- var TAP_SLOP = 10;
5961
+ var TAP_SLOP2 = 10;
5049
5962
  function useGuidelyTarget(id) {
5050
5963
  const client = useContext2(GuidelyContext);
5051
5964
  const screen = useContext2(GuidelyScreenContext);
5052
- const ref = useRef5(null);
5053
- const start = useRef5(null);
5965
+ const ref = useRef6(null);
5966
+ const start = useRef6(null);
5054
5967
  useEffect8(() => {
5055
5968
  if (!client || !id) return;
5056
5969
  return client.targets.register(id, {
@@ -5070,7 +5983,7 @@ function useGuidelyTarget(id) {
5070
5983
  (e) => {
5071
5984
  const s = start.current;
5072
5985
  start.current = null;
5073
- if (s && Math.abs(e.nativeEvent.pageX - s.x) < TAP_SLOP && Math.abs(e.nativeEvent.pageY - s.y) < TAP_SLOP) client?.targets.tapped(id);
5986
+ if (s && Math.abs(e.nativeEvent.pageX - s.x) < TAP_SLOP2 && Math.abs(e.nativeEvent.pageY - s.y) < TAP_SLOP2) client?.targets.tapped(id);
5074
5987
  },
5075
5988
  [client, id]
5076
5989
  );
@@ -5078,8 +5991,8 @@ function useGuidelyTarget(id) {
5078
5991
  }
5079
5992
  function GuidelyTarget({ id, children, onLayout, onTouchStart, onTouchEnd, ...rest }) {
5080
5993
  const t = useGuidelyTarget(id);
5081
- return /* @__PURE__ */ jsx11(
5082
- View10,
5994
+ return /* @__PURE__ */ jsx12(
5995
+ View11,
5083
5996
  {
5084
5997
  ...rest,
5085
5998
  ref: t.ref,
@@ -5103,7 +6016,7 @@ function GuidelyTarget({ id, children, onLayout, onTouchStart, onTouchEnd, ...re
5103
6016
 
5104
6017
  // src/index.tsx
5105
6018
  import { createFlagsClient as createFlagsClient3, InMemoryStickyBucketStore } from "@guidelytech/flags";
5106
- import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
6019
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
5107
6020
  function deviceLocale() {
5108
6021
  try {
5109
6022
  return Intl.DateTimeFormat().resolvedOptions().locale;
@@ -5121,21 +6034,22 @@ function defaultBridge(appVersion) {
5121
6034
  appVersion
5122
6035
  },
5123
6036
  openExternal: (url) => void Linking.openURL(url).catch(() => {
5124
- })
6037
+ }),
6038
+ windowSize: () => Dimensions.get("window")
5125
6039
  };
5126
6040
  }
5127
6041
  function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPayload, flagAttributes, ...options }) {
5128
6042
  const [client, setClient] = useState8(null);
5129
- const optionsRef = useRef6(options);
6043
+ const optionsRef = useRef7(options);
5130
6044
  optionsRef.current = options;
5131
- const userRef = useRef6(user);
6045
+ const userRef = useRef7(user);
5132
6046
  userRef.current = user;
5133
6047
  const flagsOpts = options.flags === false ? null : options.flags ?? {};
5134
6048
  const payload = flagsPayload ?? flagsOpts?.payload ?? null;
5135
- const payloadRef = useRef6(payload);
6049
+ const payloadRef = useRef7(payload);
5136
6050
  payloadRef.current = payload;
5137
- const appAttributes = useMemo5(() => ({ ...flagsOpts?.attributes ?? {}, ...flagAttributes ?? {} }), [stableStringify2(flagsOpts?.attributes ?? {}), stableStringify2(flagAttributes ?? {})]);
5138
- const appAttributesRef = useRef6(appAttributes);
6051
+ const appAttributes = useMemo6(() => ({ ...flagsOpts?.attributes ?? {}, ...flagAttributes ?? {} }), [stableStringify3(flagsOpts?.attributes ?? {}), stableStringify3(flagAttributes ?? {})]);
6052
+ const appAttributesRef = useRef7(appAttributes);
5139
6053
  appAttributesRef.current = appAttributes;
5140
6054
  const { projectId, environment, apiUrl, storage } = options;
5141
6055
  useEffect9(() => {
@@ -5172,7 +6086,7 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
5172
6086
  };
5173
6087
  }, [projectId, environment, apiUrl, storage]);
5174
6088
  const userKey = user ? JSON.stringify([user.id, user.traits ?? null, user.account ?? null]) : "";
5175
- const hadUser = useRef6(false);
6089
+ const hadUser = useRef7(false);
5176
6090
  useEffect9(() => {
5177
6091
  if (!client) return;
5178
6092
  if (user && user.id) {
@@ -5186,13 +6100,18 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
5186
6100
  useEffect9(() => {
5187
6101
  client?.flags.setAttributes(appAttributes);
5188
6102
  }, [client, appAttributes]);
6103
+ useEffect9(() => {
6104
+ client?.setLocale(options.locale ?? null);
6105
+ }, [client, options.locale]);
5189
6106
  const bootstrap = useBootstrap(payload, { ...user?.id ? { ...user.traits ?? {}, id: user.id } : {}, ...appAttributes });
5190
- const flags = useMemo5(() => ({ client: client?.flags ?? null, bootstrap }), [client, bootstrap]);
5191
- return /* @__PURE__ */ jsx12(GuidelyContext.Provider, { value: client, children: /* @__PURE__ */ jsxs9(FlagsContext.Provider, { value: flags, children: [
5192
- children,
5193
- client ? /* @__PURE__ */ jsx12(GuidelyOverlay, { client, insets, launcherOffset }) : null
6107
+ const flags = useMemo6(() => ({ client: client?.flags ?? null, bootstrap }), [client, bootstrap]);
6108
+ const tapObserver = useTapObserver(client);
6109
+ return /* @__PURE__ */ jsx13(GuidelyContext.Provider, { value: client, children: /* @__PURE__ */ jsxs9(FlagsContext.Provider, { value: flags, children: [
6110
+ options.autocapture === false ? children : /* @__PURE__ */ jsx13(View12, { style: styles10.root, ...tapObserver, children }),
6111
+ client ? /* @__PURE__ */ jsx13(GuidelyOverlay, { client, insets, launcherOffset }) : null
5194
6112
  ] }) });
5195
6113
  }
6114
+ var styles10 = StyleSheet10.create({ root: { flex: 1 } });
5196
6115
  var NOOP_RESULTS = {
5197
6116
  init: Promise.resolve(),
5198
6117
  ready: Promise.resolve(),
@@ -5264,7 +6183,7 @@ function useGuidelyIdentify(userId, traits, account) {
5264
6183
  }
5265
6184
  function useGuidelyEvent(event, handler) {
5266
6185
  const client = useContext3(GuidelyContext);
5267
- const ref = useRef6(handler);
6186
+ const ref = useRef7(handler);
5268
6187
  ref.current = handler;
5269
6188
  useEffect9(() => {
5270
6189
  if (!client) return;
@@ -5279,7 +6198,7 @@ function useGuidelyScreen(name, properties) {
5279
6198
  }
5280
6199
  function useGuidelyNavigation(navigationRef, options = {}) {
5281
6200
  const client = useContext3(GuidelyContext);
5282
- const nameRef = useRef6(options.screenName);
6201
+ const nameRef = useRef7(options.screenName);
5283
6202
  nameRef.current = options.screenName;
5284
6203
  const report = () => {
5285
6204
  const route = navigationRef.getCurrentRoute?.();
@@ -5290,7 +6209,7 @@ function useGuidelyNavigation(navigationRef, options = {}) {
5290
6209
  }
5291
6210
  function useUiSelector(select, fallback) {
5292
6211
  const client = useContext3(GuidelyContext);
5293
- const selectRef = useRef6(select);
6212
+ const selectRef = useRef7(select);
5294
6213
  selectRef.current = select;
5295
6214
  const [value, setValue] = useState8(() => client ? select(client) : fallback);
5296
6215
  useEffect9(() => {
@@ -5332,14 +6251,18 @@ export {
5332
6251
  FeatureValue,
5333
6252
  FlagsProvider,
5334
6253
  GuidelyClient,
6254
+ GuidelyMask,
5335
6255
  GuidelyOverlay,
5336
6256
  GuidelyProvider,
5337
6257
  GuidelyScreenScope,
5338
6258
  GuidelyTarget,
6259
+ HeatmapBuffer,
5339
6260
  InMemoryStickyBucketStore,
5340
6261
  Messenger,
6262
+ RageClickDetector,
5341
6263
  SDK_NAME,
5342
6264
  SDK_VERSION,
6265
+ TapTracker,
5343
6266
  TargetRegistry,
5344
6267
  createFlagsClient3 as createFlagsClient,
5345
6268
  createSseParser,