@guidelytech/react-native 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -28,14 +28,18 @@ __export(index_exports, {
28
28
  FeatureValue: () => FeatureValue,
29
29
  FlagsProvider: () => FlagsProvider,
30
30
  GuidelyClient: () => GuidelyClient,
31
+ GuidelyMask: () => GuidelyMask,
31
32
  GuidelyOverlay: () => GuidelyOverlay,
32
33
  GuidelyProvider: () => GuidelyProvider,
33
34
  GuidelyScreenScope: () => GuidelyScreenScope,
34
35
  GuidelyTarget: () => GuidelyTarget,
36
+ HeatmapBuffer: () => HeatmapBuffer,
35
37
  InMemoryStickyBucketStore: () => import_flags6.InMemoryStickyBucketStore,
36
38
  Messenger: () => Messenger,
39
+ RageClickDetector: () => RageClickDetector,
37
40
  SDK_NAME: () => SDK_NAME,
38
41
  SDK_VERSION: () => SDK_VERSION,
42
+ TapTracker: () => TapTracker,
39
43
  TargetRegistry: () => TargetRegistry,
40
44
  createFlagsClient: () => import_flags6.createFlagsClient,
41
45
  createSseParser: () => createSseParser,
@@ -63,8 +67,8 @@ __export(index_exports, {
63
67
  useGuidelyTarget: () => useGuidelyTarget
64
68
  });
65
69
  module.exports = __toCommonJS(index_exports);
66
- var import_react9 = require("react");
67
- var import_react_native12 = require("react-native");
70
+ var import_react10 = require("react");
71
+ var import_react_native13 = require("react-native");
68
72
  var import_flags3 = require("@guidelytech/flags");
69
73
 
70
74
  // src/flags.tsx
@@ -156,7 +160,7 @@ function FlagsProvider({ children, client: external, payload, ...options }) {
156
160
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FlagsContext.Provider, { value, children });
157
161
  }
158
162
 
159
- // ../shared/dist/chunk-UW6NRYRY.js
163
+ // ../shared/dist/chunk-XJXXVV3C.js
160
164
  function fnv1a(input) {
161
165
  let h = 2166136261;
162
166
  for (let i = 0; i < input.length; i++) {
@@ -336,6 +340,8 @@ function resolveField(rule, ctx) {
336
340
  return ctx.now.getHours();
337
341
  case "rollout.percentage":
338
342
  return rolloutBucket(ctx.userId || ctx.anonymousId, ctx.rolloutSalt || "guidely");
343
+ case "experiment.bucket":
344
+ return rule.key ? rolloutBucket(ctx.userId || ctx.anonymousId, rule.key) : void 0;
339
345
  case "environment":
340
346
  return ctx.environment;
341
347
  case "locale":
@@ -346,6 +352,9 @@ function resolveField(rule, ctx) {
346
352
  return ctx.screen;
347
353
  case "app.version":
348
354
  return ctx.appVersion;
355
+ case "page.views":
356
+ case "feature.clicks":
357
+ return rule.key ? ctx.tagUsage?.[rule.key] ?? 0 : 0;
349
358
  default:
350
359
  return void 0;
351
360
  }
@@ -454,14 +463,14 @@ function sanitizeProperties(props) {
454
463
  for (const [k, v] of Object.entries(props)) {
455
464
  if (SENSITIVE_PROPERTY_PATTERN.test(k)) continue;
456
465
  if (typeof v === "function" || typeof v === "symbol") continue;
457
- const str = typeof v === "string" ? v : JSON.stringify(v ?? null);
458
- size += k.length + (str?.length ?? 0);
466
+ const str2 = typeof v === "string" ? v : JSON.stringify(v ?? null);
467
+ size += k.length + (str2?.length ?? 0);
459
468
  if (size > MAX_PROPERTY_BYTES) break;
460
469
  out[k.slice(0, 100)] = typeof v === "string" ? v.slice(0, 1e3) : v;
461
470
  }
462
471
  return out;
463
472
  }
464
- var SOURCEMAP_MAX_FILE_BYTES = 25 * 1024 * 1024;
473
+ var SOURCEMAP_MAX_FILE_BYTES = 100 * 1024 * 1024;
465
474
  var MAX_COMPLAINT_ATTACHMENT_BYTES = 2 * 1024 * 1024;
466
475
  var MEDIA_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
467
476
  var MEDIA_VIDEO_MAX_BYTES = 200 * 1024 * 1024;
@@ -612,9 +621,151 @@ function isArticleListed(collections, article, member) {
612
621
  }
613
622
  return true;
614
623
  }
624
+ var RC_MODULE_IDS = ["announcements", "search", "checklists", "guides", "articles", "links", "faqs"];
625
+ function orderedModules(modules) {
626
+ const known = (modules ?? []).filter((m, i, all) => RC_MODULE_IDS.includes(m.id) && all.findIndex((x) => x.id === m.id) === i);
627
+ const missing2 = RC_MODULE_IDS.filter((id) => !known.some((m) => m.id === id)).map((id) => ({ id, enabled: true }));
628
+ return [...known, ...missing2];
629
+ }
630
+ function visibleAnnouncements(items, member, now = Date.now()) {
631
+ return (items ?? []).filter((a) => {
632
+ const t = Date.parse(a.publishedAt);
633
+ return !Number.isNaN(t) && t <= now && matchesAudiences(a.audienceIds, member);
634
+ }).sort((a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt));
635
+ }
636
+ function unreadAnnouncements(items, lastSeenAt) {
637
+ const seen = lastSeenAt ? Date.parse(lastSeenAt) : NaN;
638
+ if (Number.isNaN(seen)) return items.length;
639
+ return items.filter((a) => Date.parse(a.publishedAt) > seen).length;
640
+ }
615
641
  var CAPTURE_SCREENSHOT_MAX_BYTES = 8 * 1024 * 1024;
642
+ var DEFAULT_LOCALE = "en";
643
+ function normalizeLocale(input) {
644
+ if (typeof input !== "string") return "";
645
+ const parts = input.trim().replace(/_/g, "-").split("-").filter(Boolean);
646
+ if (!parts.length || !/^[a-zA-Z]{2,3}$/.test(parts[0])) return "";
647
+ return parts.slice(0, 3).map((p, i) => i === 0 ? p.toLowerCase() : p.length === 4 ? p[0].toUpperCase() + p.slice(1).toLowerCase() : p.length === 2 ? p.toUpperCase() : p).join("-");
648
+ }
649
+ function baseLanguage(locale) {
650
+ return normalizeLocale(locale).split("-")[0] ?? "";
651
+ }
652
+ function resolveLocale(requested, available, defaultLocale = DEFAULT_LOCALE) {
653
+ const def = normalizeLocale(defaultLocale) || DEFAULT_LOCALE;
654
+ const avail = available.map(normalizeLocale).filter(Boolean);
655
+ const all = [def, ...avail.filter((a) => a.toLowerCase() !== def.toLowerCase())];
656
+ for (const raw of requested) {
657
+ const want = normalizeLocale(raw);
658
+ if (!want) continue;
659
+ const exact = all.find((a) => a.toLowerCase() === want.toLowerCase());
660
+ if (exact) return exact;
661
+ const base = baseLanguage(want);
662
+ const sameBase = all.find((a) => a.toLowerCase() === base) ?? all.find((a) => baseLanguage(a) === base);
663
+ if (sameBase) return sameBase;
664
+ }
665
+ return def;
666
+ }
667
+ function visitorLocales(explicit, traits, platform = []) {
668
+ const out = [];
669
+ const push = (v) => {
670
+ const n = normalizeLocale(v);
671
+ if (n && !out.includes(n)) out.push(n);
672
+ };
673
+ push(explicit);
674
+ push(traits?.language);
675
+ push(traits?.locale);
676
+ for (const p of platform) push(p);
677
+ return out;
678
+ }
679
+ function guideLocales(config) {
680
+ const t = config.localization?.translations ?? {};
681
+ return Object.keys(t).filter((l) => Object.values(t[l] ?? {}).some((v) => typeof v === "string" && v.trim() !== ""));
682
+ }
683
+ function pick(map, key, fallback) {
684
+ const v = map?.[key];
685
+ return typeof v === "string" && v.trim() !== "" ? v : fallback;
686
+ }
687
+ function applyGuideTranslations(config, map) {
688
+ if (!map || !Object.keys(map).length) return config;
689
+ return {
690
+ ...config,
691
+ steps: config.steps.map((s) => ({
692
+ ...s,
693
+ title: pick(map, `steps.${s.id}.title`, s.title),
694
+ body: pick(map, `steps.${s.id}.body`, s.body),
695
+ customHtml: pick(map, `steps.${s.id}.customHtml`, s.customHtml),
696
+ imageAlt: pick(map, `steps.${s.id}.imageAlt`, s.imageAlt),
697
+ buttons: s.buttons.map((b) => ({ ...b, label: pick(map, `steps.${s.id}.buttons.${b.id}.label`, b.label) }))
698
+ })),
699
+ checklist: config.checklist ? {
700
+ ...config.checklist,
701
+ launcherLabel: pick(map, "checklist.launcherLabel", config.checklist.launcherLabel),
702
+ completionTitle: pick(map, "checklist.completionTitle", config.checklist.completionTitle),
703
+ completionBody: pick(map, "checklist.completionBody", config.checklist.completionBody),
704
+ tasks: config.checklist.tasks.map((t) => ({ ...t, title: pick(map, `checklist.tasks.${t.id}.title`, t.title), description: pick(map, `checklist.tasks.${t.id}.description`, t.description) }))
705
+ } : config.checklist,
706
+ survey: config.survey ? {
707
+ ...config.survey,
708
+ thankYouTitle: pick(map, "survey.thankYouTitle", config.survey.thankYouTitle),
709
+ thankYouBody: pick(map, "survey.thankYouBody", config.survey.thankYouBody),
710
+ questions: config.survey.questions.map((q) => ({
711
+ ...q,
712
+ title: pick(map, `survey.questions.${q.id}.title`, q.title),
713
+ description: pick(map, `survey.questions.${q.id}.description`, q.description),
714
+ placeholder: pick(map, `survey.questions.${q.id}.placeholder`, q.placeholder),
715
+ lowLabel: pick(map, `survey.questions.${q.id}.lowLabel`, q.lowLabel),
716
+ highLabel: pick(map, `survey.questions.${q.id}.highLabel`, q.highLabel),
717
+ options: q.options.map((o) => ({ ...o, label: pick(map, `survey.questions.${q.id}.options.${o.id}.label`, o.label) }))
718
+ }))
719
+ } : config.survey
720
+ };
721
+ }
722
+ function localizeGuide(guide, requested, defaultLocale = DEFAULT_LOCALE) {
723
+ const translations = guide.config.localization?.translations ?? {};
724
+ const locale = resolveLocale(requested, guideLocales(guide.config), defaultLocale);
725
+ const key = Object.keys(translations).find((l) => normalizeLocale(l).toLowerCase() === locale.toLowerCase());
726
+ if (!key) return { ...guide, locale };
727
+ return { ...guide, locale, config: applyGuideTranslations(guide.config, translations[key]) };
728
+ }
729
+ function resourceCenterLocales(rc) {
730
+ const set = new Set(Object.keys(rc.translations ?? {}));
731
+ for (const a of rc.articles) for (const l of Object.keys(a.translations ?? {})) set.add(l);
732
+ for (const a of rc.announcements ?? []) for (const l of Object.keys(a.translations ?? {})) set.add(l);
733
+ return [...set];
734
+ }
735
+ function translationFor(map, locale) {
736
+ if (!map) return void 0;
737
+ const key = Object.keys(map).find((l) => normalizeLocale(l).toLowerCase() === locale.toLowerCase());
738
+ return key ? map[key] : void 0;
739
+ }
740
+ function localizeArticle(a, locale) {
741
+ const t = translationFor(a.translations, locale);
742
+ if (!t) return a;
743
+ const body = t.body?.trim() ? t.body : a.body;
744
+ return { ...a, title: t.title?.trim() ? t.title : a.title, summary: t.summary?.trim() ? t.summary : a.summary, body, ...t.bodyOmitted ? { bodyOmitted: true } : {} };
745
+ }
746
+ function localizeAnnouncement(a, locale) {
747
+ const t = translationFor(a.translations, locale);
748
+ if (!t) return a;
749
+ return { ...a, title: t.title?.trim() ? t.title : a.title, body: t.body?.trim() ? t.body : a.body };
750
+ }
751
+ function localizeResourceCenter(rc, requested, defaultLocale = DEFAULT_LOCALE) {
752
+ const locale = resolveLocale(requested, resourceCenterLocales(rc), defaultLocale);
753
+ if (locale.toLowerCase() === normalizeLocale(defaultLocale).toLowerCase()) return { ...rc, locale };
754
+ const map = translationFor(rc.translations, locale);
755
+ return {
756
+ ...rc,
757
+ locale,
758
+ title: pick(map, "title", rc.title),
759
+ launcher: { ...rc.launcher, label: pick(map, "launcher.label", rc.launcher.label) },
760
+ collections: rc.collections.map((c) => ({ ...c, name: pick(map, `collections.${c.id}.name`, c.name), description: pick(map, `collections.${c.id}.description`, c.description) })),
761
+ links: rc.links.map((l) => ({ ...l, label: pick(map, `links.${l.id}.label`, l.label) })),
762
+ faqs: rc.faqs.map((f) => ({ ...f, question: pick(map, `faqs.${f.id}.question`, f.question), answer: pick(map, `faqs.${f.id}.answer`, f.answer) })),
763
+ articles: rc.articles.map((a) => localizeArticle(a, locale)),
764
+ announcements: rc.announcements?.map((a) => localizeAnnouncement(a, locale))
765
+ };
766
+ }
616
767
 
617
- // ../sdk-core/dist/chunk-C4WWSF67.js
768
+ // ../sdk-core/dist/chunk-XML74K2P.js
618
769
  var __defProp2 = Object.defineProperty;
619
770
  var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
620
771
  var __publicField2 = (obj, key, value) => __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -912,6 +1063,8 @@ var KEYS = {
912
1063
  queue: (key) => `guidely:queue:${key}`,
913
1064
  /** Survey answers and guide/checklist progress not yet accepted by the server. */
914
1065
  outbox: (key) => `guidely:outbox:${key}`,
1066
+ /** Agent Analytics turns not yet accepted by the server. */
1067
+ agentOutbox: (key) => `guidely:agents:${key}`,
915
1068
  session: "guidely:session",
916
1069
  active: "guidely:active",
917
1070
  authoring: "guidely:authoring",
@@ -921,10 +1074,12 @@ var KEYS = {
921
1074
  /** Messenger conversations this browser can read: [{ id, token }]. */
922
1075
  conversations: (key) => `guidely:conversations:${key}`,
923
1076
  /** Announcement ids opened in the messenger's News tab. */
924
- newsSeen: (key) => `guidely:news:${key}`
1077
+ newsSeen: (key) => `guidely:news:${key}`,
1078
+ /** Where the visitor dragged the messenger launcher ({ side, y }). */
1079
+ launcher: (key) => `guidely:launcher:${key}`
925
1080
  };
926
1081
 
927
- // ../sdk-core/dist/chunk-GDJB466J.js
1082
+ // ../sdk-core/dist/chunk-2E2YUQAG.js
928
1083
  var EventQueue = class {
929
1084
  constructor(opts) {
930
1085
  __publicField2(this, "opts", opts);
@@ -1200,12 +1355,32 @@ function areMutuallyExclusive(a, b) {
1200
1355
  return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
1201
1356
  }
1202
1357
  function isWithinSchedule(guide, now = Date.now()) {
1203
- const s = guide.config.survey;
1204
- if (!s) return true;
1205
- if (s.startAt && now < Date.parse(s.startAt)) return false;
1206
- if (s.endAt && now > Date.parse(s.endAt)) return false;
1358
+ for (const w of [guide.config.schedule, guide.config.survey]) {
1359
+ if (!w) continue;
1360
+ if (w.startAt && now < Date.parse(w.startAt)) return false;
1361
+ if (w.endAt && now > Date.parse(w.endAt)) return false;
1362
+ }
1207
1363
  return true;
1208
1364
  }
1365
+ var MINUTE = 60 * 1e3;
1366
+ function countsTowardThrottle(guide) {
1367
+ return isExclusiveGuide(guide) && !guide.config.frequency.ignoreThrottling;
1368
+ }
1369
+ function isThrottleEligible(settings, launches, now = Date.now()) {
1370
+ if (!settings || !settings.enabled) return true;
1371
+ const times = launches.map((l) => Date.parse(l)).filter((t) => !Number.isNaN(t));
1372
+ const windowMs = Math.max(1, settings.windowHours) * HOUR;
1373
+ if (times.filter((t) => now - t < windowMs).length >= Math.max(1, settings.maxGuides)) return false;
1374
+ if (settings.minGapMinutes > 0 && times.some((t) => now - t < settings.minGapMinutes * MINUTE)) return false;
1375
+ return true;
1376
+ }
1377
+ function pruneLaunches(launches, settings, now = Date.now()) {
1378
+ const keepMs = Math.max(settings?.windowHours ?? 24, 1) * HOUR + (settings?.minGapMinutes ?? 0) * MINUTE;
1379
+ return launches.filter((l) => {
1380
+ const t = Date.parse(l);
1381
+ return !Number.isNaN(t) && now - t < keepMs;
1382
+ }).slice(-100);
1383
+ }
1209
1384
  function dismissedAfterMerge(local, server) {
1210
1385
  if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
1211
1386
  return local?.dismissedAt ?? server.dismissedAt ?? null;
@@ -1214,6 +1389,158 @@ function dismissedAfterMerge(local, server) {
1214
1389
  // src/core/client.ts
1215
1390
  var import_flags2 = require("@guidelytech/flags");
1216
1391
 
1392
+ // src/core/launcherPlacement.ts
1393
+ var LAUNCHER_PLACEMENT_KEY = "guidely:messenger.launcher";
1394
+ var LAUNCHER_MARGIN = 16;
1395
+ var LAUNCHER_DRAG_THRESHOLD = 8;
1396
+ var clamp2 = (v, min, max) => Math.min(max, Math.max(min, v));
1397
+ function sideX(f, side) {
1398
+ return side === "left" ? f.insets.left + LAUNCHER_MARGIN : f.width - f.insets.right - LAUNCHER_MARGIN - f.size;
1399
+ }
1400
+ function verticalRange(f) {
1401
+ const max = f.height - f.defaultBottom - f.size;
1402
+ return { min: Math.min(max, f.insets.top + LAUNCHER_MARGIN), max };
1403
+ }
1404
+ function clampToSafeArea(f, p) {
1405
+ const left = f.insets.left + LAUNCHER_MARGIN;
1406
+ const top = f.insets.top + LAUNCHER_MARGIN;
1407
+ return {
1408
+ x: clamp2(p.x, left, Math.max(left, f.width - f.insets.right - LAUNCHER_MARGIN - f.size)),
1409
+ y: clamp2(p.y, top, Math.max(top, f.height - f.insets.bottom - LAUNCHER_MARGIN - f.size))
1410
+ };
1411
+ }
1412
+ function snapPlacement(f, p) {
1413
+ const { min, max } = verticalRange(f);
1414
+ const side = p.x + f.size / 2 < f.width / 2 ? "left" : "right";
1415
+ return { side, fraction: max > min ? (max - clamp2(p.y, min, max)) / (max - min) : 0 };
1416
+ }
1417
+ function placementPoint(f, placement) {
1418
+ const { min, max } = verticalRange(f);
1419
+ return { x: sideX(f, placement.side), y: max - clamp2(placement.fraction, 0, 1) * (max - min) };
1420
+ }
1421
+ function parsePlacement(raw) {
1422
+ if (!raw) return null;
1423
+ try {
1424
+ const v = JSON.parse(raw);
1425
+ if (!v || typeof v !== "object" || v.side !== "left" && v.side !== "right" || typeof v.fraction !== "number" || !Number.isFinite(v.fraction)) return null;
1426
+ return { side: v.side, fraction: clamp2(v.fraction, 0, 1) };
1427
+ } catch {
1428
+ return null;
1429
+ }
1430
+ }
1431
+ function serializePlacement(p) {
1432
+ return JSON.stringify({ side: p.side, fraction: Math.round(clamp2(p.fraction, 0, 1) * 1e4) / 1e4 });
1433
+ }
1434
+
1435
+ // src/core/autocapture.ts
1436
+ var TAP_SLOP = 10;
1437
+ var TAP_MAX_MS = 1e3;
1438
+ var HEATMAP_MAX_POINTS = 40;
1439
+ function quoteValue(value) {
1440
+ return `"${value.replace(/["\\]/g, "\\$&")}"`;
1441
+ }
1442
+ function attrSelector(name, value) {
1443
+ return `[${name}=${quoteValue(value)}]`.slice(0, 300);
1444
+ }
1445
+ function cleanText(text) {
1446
+ const t = (text ?? "").replace(/\s+/g, " ").trim();
1447
+ return t ? t.slice(0, 100) : void 0;
1448
+ }
1449
+ function tapProperties(el, x, y, platform, maskAll) {
1450
+ const props = { selector: el.selector.slice(0, 300), tag: el.tag || "View", x: Math.round(x) || 0, y: Math.round(y) || 0, platform };
1451
+ const text = maskAll ? void 0 : cleanText(el.text);
1452
+ if (text) props.text = text;
1453
+ return props;
1454
+ }
1455
+ var r2 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 100) / 100;
1456
+ function heatmapPoint(x, y, el, rect, rage) {
1457
+ const p = { k: rage ? "r" : "c", x: Math.round(x) || 0, y: Math.round(y) || 0 };
1458
+ const s = el?.selector.slice(0, 120);
1459
+ if (s) p.s = s;
1460
+ if (s && rect && rect.width > 0 && rect.height > 0) {
1461
+ p.rx = r2((x - rect.x) / rect.width);
1462
+ p.ry = r2((y - rect.y) / rect.height);
1463
+ }
1464
+ return p;
1465
+ }
1466
+ var RageClickDetector = class {
1467
+ constructor(threshold = 3, windowMs = 1e3, radius = 30) {
1468
+ __publicField(this, "threshold", threshold);
1469
+ __publicField(this, "windowMs", windowMs);
1470
+ __publicField(this, "radius", radius);
1471
+ __publicField(this, "clicks", []);
1472
+ __publicField(this, "fired", false);
1473
+ }
1474
+ /** Taps in the current burst once it has fired (rage), otherwise 0. */
1475
+ get burst() {
1476
+ return this.fired ? this.clicks.length : 0;
1477
+ }
1478
+ /** Records a tap; returns the tap count when it completes a burst, otherwise 0. */
1479
+ add(x, y, t) {
1480
+ const first = this.clicks[0];
1481
+ const last = this.clicks[this.clicks.length - 1];
1482
+ if (!first || !last || t - last.t > this.windowMs || Math.hypot(x - first.x, y - first.y) > this.radius) {
1483
+ this.clicks = [];
1484
+ this.fired = false;
1485
+ }
1486
+ this.clicks = this.clicks.filter((c) => t - c.t <= this.windowMs);
1487
+ this.clicks.push({ x, y, t });
1488
+ if (this.fired || this.clicks.length < this.threshold) return 0;
1489
+ this.fired = true;
1490
+ return this.clicks.length;
1491
+ }
1492
+ };
1493
+ var HeatmapBuffer = class {
1494
+ constructor() {
1495
+ /** Screen path the buffered points belong to. */
1496
+ __publicField(this, "url", "");
1497
+ __publicField(this, "points", []);
1498
+ }
1499
+ /** Adds a tap (`burst` > 0 marks it and the burst's previous taps as rage); true when the batch is full. */
1500
+ add(url, point, burst) {
1501
+ this.url = url;
1502
+ for (let i = Math.max(0, this.points.length - burst + 1); i < this.points.length; i++) this.points[i].k = "r";
1503
+ this.points.push(point);
1504
+ return this.points.length >= HEATMAP_MAX_POINTS;
1505
+ }
1506
+ /** Empties the buffer, returning what was in it. */
1507
+ take() {
1508
+ const points = this.points;
1509
+ this.points = [];
1510
+ return points.length ? { url: this.url, points } : null;
1511
+ }
1512
+ };
1513
+ var TapTracker = class {
1514
+ constructor() {
1515
+ __publicField(this, "start", null);
1516
+ __publicField(this, "moved", 0);
1517
+ }
1518
+ /** A finger went down. A second finger while one is tracked cancels the gesture (not a tap). */
1519
+ begin(x, y, t, touches, id) {
1520
+ if (this.start || touches > 1) {
1521
+ this.start = null;
1522
+ return;
1523
+ }
1524
+ this.start = { x, y, t, id };
1525
+ this.moved = 0;
1526
+ }
1527
+ move(x, y, id) {
1528
+ const s = this.start;
1529
+ 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));
1530
+ }
1531
+ /** The finger lifted: the tap position when it moved ≤ 10pt and lasted < 1s, else null. */
1532
+ end(x, y, t, id) {
1533
+ const s = this.start;
1534
+ if (!s || id !== void 0 && s.id !== void 0 && id !== s.id) return null;
1535
+ this.start = null;
1536
+ const moved = Math.max(this.moved, Math.hypot(x - s.x, y - s.y));
1537
+ return moved <= TAP_SLOP && t - s.t < TAP_MAX_MS ? { x, y } : null;
1538
+ }
1539
+ cancel() {
1540
+ this.start = null;
1541
+ }
1542
+ };
1543
+
1217
1544
  // src/core/targets.ts
1218
1545
  var TargetTimeoutError = class extends Error {
1219
1546
  constructor(ids) {
@@ -1316,6 +1643,35 @@ var TargetRegistry = class {
1316
1643
  return null;
1317
1644
  }
1318
1645
  }
1646
+ /**
1647
+ * The registered target on the current screen whose visible frame contains the point (window
1648
+ * coordinates); the smallest one when targets are nested. Frames that do not answer within
1649
+ * `timeoutMs` are skipped.
1650
+ */
1651
+ async hitTest(x, y, timeoutMs = 500) {
1652
+ const candidates = [];
1653
+ this.targets.forEach((list, id) => {
1654
+ for (const handle of list) if (!(handle.screen && this.screen && handle.screen !== this.screen)) candidates.push({ id, handle });
1655
+ });
1656
+ if (!candidates.length) return null;
1657
+ const rects = await Promise.all(
1658
+ candidates.map(
1659
+ ({ handle }) => new Promise((resolve) => {
1660
+ const timer = setTimeout(() => resolve(null), timeoutMs);
1661
+ void this.measureHandle(handle).then((r) => {
1662
+ clearTimeout(timer);
1663
+ resolve(r);
1664
+ });
1665
+ })
1666
+ )
1667
+ );
1668
+ let best = null;
1669
+ rects.forEach((r, i) => {
1670
+ if (!r || x < r.x || y < r.y || x > r.x + r.width || y > r.y + r.height) return;
1671
+ if (!best || r.width * r.height < best.rect.width * best.rect.height) best = { id: candidates[i].id, rect: r };
1672
+ });
1673
+ return best;
1674
+ }
1319
1675
  /** First of `ids` that is mounted and visible. */
1320
1676
  async resolve(ids) {
1321
1677
  for (const id of ids) {
@@ -2073,7 +2429,8 @@ var Messenger = class {
2073
2429
  this.stopThreadPolling();
2074
2430
  this.set({ open: true, view: { kind: "article", articleId: id, from }, article: null });
2075
2431
  try {
2076
- const res = await this.request("GET", `/articles/${encodeURIComponent(id)}`);
2432
+ const locale = this.deps.locale?.();
2433
+ const res = await this.request("GET", `/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : {});
2077
2434
  this.set({ article: res.article });
2078
2435
  } catch (err) {
2079
2436
  this.deps.onError(err);
@@ -2188,7 +2545,7 @@ var PersistentStore = class {
2188
2545
 
2189
2546
  // src/core/client.ts
2190
2547
  var SDK_NAME = "@guidelytech/react-native";
2191
- var SDK_VERSION = "0.10.0";
2548
+ var SDK_VERSION = "0.12.0";
2192
2549
  var DEFAULT_API_URL = "https://guidelytech.com";
2193
2550
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
2194
2551
  var CACHED_START_MS = 1500;
@@ -2202,6 +2559,8 @@ function settlesWithin(promise, ms) {
2202
2559
  promise.then(done, done);
2203
2560
  });
2204
2561
  }
2562
+ var appVersionKey = (key) => `guidely:app:${key}`;
2563
+ var UNKNOWN_VIEW = { selector: "View", tag: "View" };
2205
2564
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
2206
2565
  var emptyState = () => ({ guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] });
2207
2566
  var newSession = () => ({ id: `s_${randomId(16)}`, startedAt: nowIso(), lastActiveAt: nowIso(), shownGuides: [], events: [] });
@@ -2233,6 +2592,7 @@ var GuidelyClient = class {
2233
2592
  __publicField(this, "activeSurvey", null);
2234
2593
  __publicField(this, "queued", []);
2235
2594
  __publicField(this, "flows", /* @__PURE__ */ new Map());
2595
+ /** Checklists on screen, with the guide as rendered (in the visitor's language). */
2236
2596
  __publicField(this, "checklists", /* @__PURE__ */ new Map());
2237
2597
  __publicField(this, "expanded", /* @__PURE__ */ new Set());
2238
2598
  __publicField(this, "surveyView", null);
@@ -2252,6 +2612,27 @@ var GuidelyClient = class {
2252
2612
  __publicField(this, "flags");
2253
2613
  /** Experiments already reported this session (`experiment_viewed` is sent once per session). */
2254
2614
  __publicField(this, "exposures", /* @__PURE__ */ new Set());
2615
+ // Product analytics (autocapture).
2616
+ __publicField(this, "rage", new RageClickDetector());
2617
+ __publicField(this, "heatmap", new HeatmapBuffer());
2618
+ /** Taps are processed in order (target frames are measured asynchronously). */
2619
+ __publicField(this, "tapChain", Promise.resolve());
2620
+ /** When the current screen's foreground measurement started; `screenLeft` once it has been reported. */
2621
+ __publicField(this, "screenStartedAt", 0);
2622
+ __publicField(this, "screenLeft", true);
2623
+ /** Last `app_open` (cold start or return to the foreground), for `app_background.foregroundMs`. */
2624
+ __publicField(this, "openedAt", Date.now());
2625
+ __publicField(this, "launchChecked", false);
2626
+ /** An identity was stored before this launch (an existing install adopting lifecycle events). */
2627
+ __publicField(this, "hadIdentity", false);
2628
+ /** Language set with `setLocale()` (or the `locale` option). */
2629
+ __publicField(this, "locale", null);
2630
+ /** Language each shown guide was rendered in; added to its events as `locale`. */
2631
+ __publicField(this, "guideLocales", /* @__PURE__ */ new Map());
2632
+ /** The resource center in the visitor's language, recomputed when the config or language changes. */
2633
+ __publicField(this, "localizedRc", null);
2634
+ /** "What's new" last-seen time when the resource center opened (entries after it read as new). */
2635
+ __publicField(this, "announcementsSeenAtOpen", null);
2255
2636
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
2256
2637
  __publicField(this, "eventBus", {
2257
2638
  on: (name, cb) => {
@@ -2281,6 +2662,7 @@ var GuidelyClient = class {
2281
2662
  if (!options.storage) this.log("no `storage` passed: identity, guide history, queued events and survey answers last only for this app session, so whatever could not be sent during a Guidely outage is lost when the app restarts. Pass AsyncStorage to persist them.");
2282
2663
  this.cleanups.push(this.targets.onTap((id) => this.handleTap(id)));
2283
2664
  if (options.user) this.user = { ...options.user };
2665
+ this.locale = options.locale ? normalizeLocale(options.locale) || null : null;
2284
2666
  this.flags = this.createFlags(options.flags);
2285
2667
  }
2286
2668
  createFlags(opts) {
@@ -2329,9 +2711,10 @@ var GuidelyClient = class {
2329
2711
  async bootstrap() {
2330
2712
  const key = this.options.projectId;
2331
2713
  try {
2332
- await this.store.hydrate([KEYS.anon, KEYS.seen, KEYS.session, KEYS.config(key), KEYS.etag(key), KEYS.state(key), KEYS.user(key), KEYS.queue(key), KEYS.outbox(key), KEYS.conversations(key)]);
2714
+ 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), LAUNCHER_PLACEMENT_KEY]);
2333
2715
  if (this.destroyed) return;
2334
2716
  this.anonymousId = this.store.get(KEYS.anon) || "";
2717
+ this.hadIdentity = !!this.anonymousId;
2335
2718
  if (!this.anonymousId) {
2336
2719
  this.anonymousId = `anon_${randomId(20)}`;
2337
2720
  this.store.set(KEYS.anon, this.anonymousId);
@@ -2368,6 +2751,7 @@ var GuidelyClient = class {
2368
2751
  if (!this.config) await fresh;
2369
2752
  else if (!await settlesWithin(fresh, CACHED_START_MS)) void fresh.then(() => this.applyConfigChange(before));
2370
2753
  if (this.destroyed) return;
2754
+ this.checkLaunch();
2371
2755
  this.wireTimers();
2372
2756
  if (this.user?.id) await settlesWithin(this.identify(this.user.id, this.user.traits, this.user.account), CACHED_START_MS);
2373
2757
  if (this.destroyed) return;
@@ -2397,6 +2781,7 @@ var GuidelyClient = class {
2397
2781
  if (res.etag) this.store.set(KEYS.etag(this.options.projectId), res.etag);
2398
2782
  else this.store.remove(KEYS.etag(this.options.projectId));
2399
2783
  this.syncMessenger();
2784
+ if (this.hydrated) this.checkLaunch();
2400
2785
  this.emitter.emit("config", this.config);
2401
2786
  this.log("config loaded", this.config.configVersion, `${this.config.guides.length} guides`);
2402
2787
  return true;
@@ -2417,7 +2802,7 @@ var GuidelyClient = class {
2417
2802
  if (!this.isReady || !this.config || this.backgroundedAt !== null) return;
2418
2803
  const url = this.currentUrl();
2419
2804
  const ctx = this.getContext();
2420
- for (const guide of this.config.guides) if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
2805
+ for (const guide of this.candidates()) if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
2421
2806
  this.drainQueue(url, ctx);
2422
2807
  }, 1e3);
2423
2808
  this.cleanups.push(() => clearInterval(poll));
@@ -2441,7 +2826,9 @@ var GuidelyClient = class {
2441
2826
  setAppState(state) {
2442
2827
  if (this.destroyed || !this.hydrated) return;
2443
2828
  if (state === "background") {
2829
+ const wasForeground = this.backgroundedAt === null;
2444
2830
  this.backgroundedAt = Date.now();
2831
+ if (wasForeground) this.wentToBackground();
2445
2832
  this.session.lastActiveAt = nowIso();
2446
2833
  this.persistSession();
2447
2834
  void this.queue?.flush();
@@ -2458,6 +2845,7 @@ var GuidelyClient = class {
2458
2845
  this.persistSession();
2459
2846
  this.firedDelays.clear();
2460
2847
  }
2848
+ this.cameToForeground();
2461
2849
  void this.flags.refresh();
2462
2850
  if (Date.now() - this.configFetchedAt > this.options.configRefreshMs && this.api.health.canSend()) void this.refresh();
2463
2851
  else if (this.isReady) this.evaluate("ready");
@@ -2470,6 +2858,7 @@ var GuidelyClient = class {
2470
2858
  /** Tears everything down. Create a new client to start again. */
2471
2859
  destroy() {
2472
2860
  if (this.destroyed) return;
2861
+ this.flushHeatmap();
2473
2862
  this.destroyed = true;
2474
2863
  this.active?.controller.destroy();
2475
2864
  this.active = null;
@@ -2521,6 +2910,7 @@ var GuidelyClient = class {
2521
2910
  for (const [id, s] of Object.entries(data.guideState || {})) {
2522
2911
  const local = guides[id];
2523
2912
  guides[id] = {
2913
+ ...local,
2524
2914
  impressions: Math.max(local?.impressions ?? 0, s.impressions ?? 0),
2525
2915
  completedAt: local?.completedAt ?? s.completedAt ?? null,
2526
2916
  dismissedAt: local?.dismissedAt ?? s.dismissedAt ?? null,
@@ -2542,7 +2932,10 @@ var GuidelyClient = class {
2542
2932
  checklists,
2543
2933
  surveysSubmitted: Array.from(/* @__PURE__ */ new Set([...this.state.surveysSubmitted, ...data.surveysSubmitted || []])),
2544
2934
  audienceIds: data.audienceIds || [],
2545
- firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt
2935
+ firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt,
2936
+ // Device-local only (the server doesn't hold them).
2937
+ ...this.state.throttle ? { throttle: this.state.throttle } : {},
2938
+ ...this.state.announcementsSeenAt ? { announcementsSeenAt: this.state.announcementsSeenAt } : {}
2546
2939
  };
2547
2940
  if (data.firstSeenAt && Date.now() - Date.parse(data.firstSeenAt) > 6e4) this.firstVisit = false;
2548
2941
  this.persistState();
@@ -2634,6 +3027,8 @@ var GuidelyClient = class {
2634
3027
  ...extra
2635
3028
  };
2636
3029
  if (extra.properties) event.properties = sanitizeProperties(extra.properties);
3030
+ const locale = extra.guideId ? this.guideLocales.get(extra.guideId) : void 0;
3031
+ if (locale) event.properties = { ...event.properties ?? {}, locale };
2637
3032
  this.queue.enqueue(event);
2638
3033
  this.emitter.emit("track", event);
2639
3034
  return event;
@@ -2666,11 +3061,19 @@ var GuidelyClient = class {
2666
3061
  if (!name) return;
2667
3062
  this.whenHydrated(() => {
2668
3063
  const changed = name !== this.screenName;
3064
+ if (changed) {
3065
+ this.leaveScreen();
3066
+ this.flushHeatmap();
3067
+ }
2669
3068
  this.screenName = name;
2670
3069
  this.targets.setScreen(name);
2671
3070
  if (changed) this.syncFlagAttributes();
2672
3071
  const url = this.currentUrl();
2673
- if (changed) this.enqueue("page_view", { properties: { ...properties, path: url, screen: name } });
3072
+ if (changed) {
3073
+ this.screenStartedAt = Date.now();
3074
+ this.screenLeft = this.backgroundedAt !== null;
3075
+ if (this.pageviewsOn()) this.enqueue("page_view", { properties: { ...properties, path: url, screen: name, ...this.deviceProperties() } });
3076
+ }
2674
3077
  this.completeChecklistTasksBy("url_visited", url);
2675
3078
  this.active?.controller.resume();
2676
3079
  this.flows.forEach((f) => f.controller.resume());
@@ -2698,6 +3101,149 @@ var GuidelyClient = class {
2698
3101
  if (this.isReady) this.evaluate("click", { targetId });
2699
3102
  }
2700
3103
  // ---------------------------------------------------------------------------
3104
+ // Product analytics (autocapture)
3105
+ // ---------------------------------------------------------------------------
3106
+ captureAllowed() {
3107
+ return !this.destroyed && this.analyticsEnabled();
3108
+ }
3109
+ /** A capability runs when the project enables it (absent on old servers → off), no option turns it off and analytics are allowed. */
3110
+ autocaptureOn(key) {
3111
+ const o = this.options.autocapture;
3112
+ return !!this.config?.settings?.autocapture?.[key] && o !== false && !(typeof o === "object" && o?.[key] === false) && this.captureAllowed();
3113
+ }
3114
+ /** `page_view` from `screen()` predates autocapture: only an explicit opt-out stops it. */
3115
+ pageviewsOn() {
3116
+ const o = this.options.autocapture;
3117
+ return this.config?.settings?.autocapture?.pageviews !== false && o !== false && !(typeof o === "object" && o?.pageviews === false);
3118
+ }
3119
+ /** True when taps are worth observing (clicks, rage taps or heatmaps are on); the provider skips resolving views otherwise. */
3120
+ isTapCaptureOn() {
3121
+ return this.hydrated && (this.autocaptureOn("clicks") || this.autocaptureOn("rageClicks") || this.autocaptureOn("heatmaps"));
3122
+ }
3123
+ /**
3124
+ * Records a tap observed by the provider: `click` for an interactive element, `rage_click` for
3125
+ * bursts and a heatmap point. A registered `<GuidelyTarget>` whose frame contains the point
3126
+ * (smallest wins) takes precedence over the resolved element and is reported as `#<id>`.
3127
+ */
3128
+ recordTap(tap) {
3129
+ try {
3130
+ if (!this.hydrated || this.destroyed || !Number.isFinite(tap.x) || !Number.isFinite(tap.y)) return;
3131
+ const on = { clicks: this.autocaptureOn("clicks"), rage: this.autocaptureOn("rageClicks"), heat: this.autocaptureOn("heatmaps") };
3132
+ if (!on.clicks && !on.rage && !on.heat) return;
3133
+ const t = tap.t ?? Date.now();
3134
+ const url = this.screenName ? this.currentUrl() : "";
3135
+ this.tapChain = this.tapChain.then(() => this.processTap(tap, t, url, on)).catch((err) => this.reportError(err));
3136
+ } catch (err) {
3137
+ this.reportError(err);
3138
+ }
3139
+ }
3140
+ async processTap(tap, t, url, on) {
3141
+ let el = tap.element ?? null;
3142
+ let rect = null;
3143
+ const target = this.targets.ids().length ? await this.targets.hitTest(tap.x, tap.y) : null;
3144
+ if (target) {
3145
+ el = { selector: `#${target.id}`, tag: el?.tag ?? tap.hit?.tag ?? "View", text: el?.text };
3146
+ rect = target.rect;
3147
+ }
3148
+ if (this.destroyed) return;
3149
+ const platform = this.bridge.device.platform;
3150
+ const maskAll = !!this.config?.settings?.replay?.maskAllText;
3151
+ const eventUrl = this.collectUrls() && url ? url : void 0;
3152
+ if (on.clicks && el) this.enqueue("click", { url: eventUrl, properties: { ...tapProperties(el, tap.x, tap.y, platform, maskAll) } });
3153
+ const count = this.rage.add(tap.x, tap.y, t);
3154
+ if (on.rage && count) this.enqueue("rage_click", { url: eventUrl, properties: { ...tapProperties(el ?? tap.hit ?? UNKNOWN_VIEW, tap.x, tap.y, platform, maskAll), count } });
3155
+ if (!on.heat) return;
3156
+ const burst = this.rage.burst;
3157
+ if (el && !rect && el.measure) rect = await el.measure().catch(() => null);
3158
+ if (this.destroyed) return;
3159
+ if (this.heatmap.url !== url) this.flushHeatmap();
3160
+ if (this.heatmap.add(url, heatmapPoint(tap.x, tap.y, el, rect, burst > 0), burst)) this.flushHeatmap();
3161
+ }
3162
+ /** Sends the buffered heatmap taps as one `heatmap` event attributed to the screen they happened on. */
3163
+ flushHeatmap() {
3164
+ const batch = this.heatmap.take();
3165
+ if (!batch || !this.captureAllowed()) return;
3166
+ const collect = this.collectUrls() && !!batch.url;
3167
+ const { width, height } = this.windowSize();
3168
+ this.enqueue("heatmap", {
3169
+ url: collect ? batch.url : void 0,
3170
+ properties: { ...collect ? { path: batch.url } : {}, viewportWidth: width, viewportHeight: height, pageHeight: height, platform: this.bridge.device.platform, points: batch.points }
3171
+ });
3172
+ }
3173
+ windowSize() {
3174
+ try {
3175
+ const s = this.bridge.windowSize?.();
3176
+ if (s && Number.isFinite(s.width) && Number.isFinite(s.height)) return { width: Math.round(s.width), height: Math.round(s.height) };
3177
+ } catch {
3178
+ }
3179
+ return { width: 0, height: 0 };
3180
+ }
3181
+ /** Ends the foreground measurement of the current screen with a `page_leave` (once per measurement). */
3182
+ leaveScreen() {
3183
+ if (!this.screenName || this.screenLeft) return;
3184
+ this.screenLeft = true;
3185
+ if (!this.autocaptureOn("pageLeave")) return;
3186
+ const url = this.currentUrl();
3187
+ const collect = this.collectUrls();
3188
+ this.enqueue("page_leave", {
3189
+ url: collect ? url : void 0,
3190
+ properties: { ...collect ? { path: url } : {}, screen: this.screenName, durationMs: Math.max(0, Date.now() - this.screenStartedAt), scrollDepth: 100, platform: this.bridge.device.platform }
3191
+ });
3192
+ }
3193
+ /** `{ appVersion, build }` of this launch, without unknown keys. */
3194
+ appInfo() {
3195
+ const appVersion = this.options.appVersion ?? this.bridge.device.appVersion;
3196
+ const build = this.options.appBuild;
3197
+ return { ...appVersion ? { appVersion: String(appVersion) } : {}, ...build ? { build: String(build) } : {} };
3198
+ }
3199
+ /**
3200
+ * Once per launch, as soon as a config is known: `app_install` / `app_update` from the stored
3201
+ * version (when the app passes `appVersion` and `storage`), then `app_open` with `coldStart: true`.
3202
+ */
3203
+ checkLaunch() {
3204
+ if (this.launchChecked || !this.config || this.destroyed || !this.hydrated) return;
3205
+ this.launchChecked = true;
3206
+ try {
3207
+ const on = this.autocaptureOn("appLifecycle");
3208
+ const info = this.appInfo();
3209
+ if (info.appVersion && this.options.storage) {
3210
+ const key = appVersionKey(this.options.projectId);
3211
+ const stored = readJson(this.store, key, null);
3212
+ if (!stored?.appVersion) {
3213
+ if (!this.hadIdentity && on) this.enqueue("app_install", { properties: { ...info } });
3214
+ } else if (stored.appVersion !== info.appVersion || !!stored.build && !!info.build && stored.build !== info.build) {
3215
+ if (on) this.enqueue("app_update", { properties: { ...info, previousVersion: stored.appVersion, ...stored.build ? { previousBuild: stored.build } : {} } });
3216
+ }
3217
+ writeJson(this.store, key, info);
3218
+ }
3219
+ if (on) this.enqueue("app_open", { properties: { ...info, coldStart: true } });
3220
+ } catch (err) {
3221
+ this.reportError(err);
3222
+ }
3223
+ }
3224
+ wentToBackground() {
3225
+ try {
3226
+ this.leaveScreen();
3227
+ this.flushHeatmap();
3228
+ if (this.launchChecked && this.autocaptureOn("appLifecycle")) this.enqueue("app_background", { properties: { foregroundMs: Math.max(0, Date.now() - this.openedAt) } });
3229
+ } catch (err) {
3230
+ this.reportError(err);
3231
+ }
3232
+ }
3233
+ cameToForeground() {
3234
+ try {
3235
+ this.openedAt = Date.now();
3236
+ if (this.launchChecked && this.autocaptureOn("appLifecycle")) this.enqueue("app_open", { properties: { ...this.appInfo(), coldStart: false } });
3237
+ if (this.screenName && this.screenLeft) {
3238
+ this.screenStartedAt = Date.now();
3239
+ this.screenLeft = false;
3240
+ if (this.pageviewsOn() && this.autocaptureOn("pageLeave")) this.enqueue("page_view", { properties: { path: this.currentUrl(), screen: this.screenName, ...this.deviceProperties(), resumed: true } });
3241
+ }
3242
+ } catch (err) {
3243
+ this.reportError(err);
3244
+ }
3245
+ }
3246
+ // ---------------------------------------------------------------------------
2701
3247
  // Targeting + triggers
2702
3248
  // ---------------------------------------------------------------------------
2703
3249
  /** Builds the targeting context for the current moment. */
@@ -2744,6 +3290,50 @@ var GuidelyClient = class {
2744
3290
  if (guide.kind === "checklist") return !this.state.checklists[guide.id]?.dismissedAt;
2745
3291
  return isFrequencyEligible(guide.config.frequency, this.state.guides[guide.id], this.session.shownGuides.includes(guide.id));
2746
3292
  }
3293
+ /** Guide throttling: may this guide launch automatically now? */
3294
+ throttleAllows(guide) {
3295
+ if (!countsTowardThrottle(guide)) return true;
3296
+ return isThrottleEligible(this.config?.settings.throttling, this.state.throttle ?? []);
3297
+ }
3298
+ recordLaunch(guide, manual) {
3299
+ if (manual || !countsTowardThrottle(guide) || !this.config?.settings.throttling?.enabled) return;
3300
+ this.state.throttle = pruneLaunches([...this.state.throttle ?? [], nowIso()], this.config.settings.throttling);
3301
+ }
3302
+ /** Guides and A/B test control groups, which are evaluated the same way but never render. */
3303
+ candidates() {
3304
+ const guides = this.config?.guides ?? [];
3305
+ const holdouts = this.config?.guideHoldouts ?? [];
3306
+ return holdouts.length ? [...guides, ...holdouts.map((h) => h.guide)] : guides;
3307
+ }
3308
+ holdoutFor(guide) {
3309
+ return this.config?.guideHoldouts?.find((h) => h.guide.id === guide.id) ?? null;
3310
+ }
3311
+ // ---------------------------------------------------------------------------
3312
+ // Localization
3313
+ // ---------------------------------------------------------------------------
3314
+ /** The visitor's language candidates, most preferred first: `setLocale()` / `locale` option, traits, device. */
3315
+ locales() {
3316
+ return visitorLocales(this.locale, this.user?.traits, [this.bridge.device.locale]);
3317
+ }
3318
+ defaultLocale() {
3319
+ return this.config?.settings?.localization?.defaultLocale || "en";
3320
+ }
3321
+ /** The guide in the visitor's language (remembered so its events carry `locale`). */
3322
+ localize(guide) {
3323
+ const localized = localizeGuide(guide, this.locales(), this.defaultLocale());
3324
+ this.guideLocales.set(guide.id, localized.locale);
3325
+ return localized;
3326
+ }
3327
+ /**
3328
+ * Sets the language for guides and the resource center (`null` = back to the user's traits and
3329
+ * the device language). Guides already on screen keep their language; the resource center re-renders.
3330
+ */
3331
+ setLocale(locale) {
3332
+ const next = locale ? normalizeLocale(locale) || null : null;
3333
+ if (next === this.locale) return;
3334
+ this.locale = next;
3335
+ this.notify();
3336
+ }
2747
3337
  triggerFires(guide, reason, detail) {
2748
3338
  const t = guide.config.trigger;
2749
3339
  switch (t.type) {
@@ -2779,7 +3369,7 @@ var GuidelyClient = class {
2779
3369
  this.lastAudiences = current;
2780
3370
  }
2781
3371
  const candidates = [];
2782
- for (const guide of this.config.guides) {
3372
+ for (const guide of this.candidates()) {
2783
3373
  const t = guide.config.trigger;
2784
3374
  if (detail.endedGuide && detail.endedGuide.id === guide.id) continue;
2785
3375
  if (reason === "ready" || reason === "navigation") {
@@ -2795,6 +3385,10 @@ var GuidelyClient = class {
2795
3385
  if (t.type === "page_load" && t.delayMs > 0 && reason === "timer" && detail.guideId !== guide.id) continue;
2796
3386
  if (!this.triggerFires(guide, reason, detail)) continue;
2797
3387
  if (!this.isEligible(guide, ctx, url)) continue;
3388
+ if (!this.throttleAllows(guide)) {
3389
+ this.log("throttled", guide.key);
3390
+ continue;
3391
+ }
2798
3392
  candidates.push(guide);
2799
3393
  }
2800
3394
  if (reason === "after_guide" || reason === "navigation") this.drainQueue(url, ctx);
@@ -2840,6 +3434,12 @@ var GuidelyClient = class {
2840
3434
  }
2841
3435
  tryShow(guide, url, manual) {
2842
3436
  if (this.destroyed) return false;
3437
+ if (!manual && !this.throttleAllows(guide)) return false;
3438
+ if (this.holdoutFor(guide)) {
3439
+ if (this.hasExclusive() && !manual) return false;
3440
+ this.recordImpression(guide, manual);
3441
+ return true;
3442
+ }
2843
3443
  if (!isExclusiveGuide(guide)) {
2844
3444
  if (this.flows.has(guide.id) || this.checklists.has(guide.id)) return false;
2845
3445
  this.show(guide, manual);
@@ -2881,14 +3481,18 @@ var GuidelyClient = class {
2881
3481
  guideState(id) {
2882
3482
  return this.state.guides[id] ?? (this.state.guides[id] = { impressions: 0 });
2883
3483
  }
2884
- recordImpression(guide) {
3484
+ recordImpression(guide, manual = false) {
2885
3485
  const s = this.guideState(guide.id);
2886
3486
  s.impressions += 1;
2887
3487
  s.lastShownAt = nowIso();
2888
3488
  if (!this.session.shownGuides.includes(guide.id)) this.session.shownGuides.push(guide.id);
3489
+ this.recordLaunch(guide, manual);
2889
3490
  this.persistState();
2890
3491
  this.persistSession();
2891
- this.syncGuideState(guide.id, { impression: true });
3492
+ const holdout = this.holdoutFor(guide);
3493
+ const experiment = holdout ? { id: holdout.experimentId, variantId: holdout.variantId } : guide.experiment;
3494
+ if (experiment) this.enqueue("experiment_viewed", { properties: { guideExperimentId: experiment.id, variantId: experiment.variantId, ...holdout ? {} : { guideId: guide.id } } });
3495
+ if (!holdout) this.syncGuideState(guide.id, { impression: true });
2892
3496
  }
2893
3497
  recordEnd(guide, reason) {
2894
3498
  const s = this.guideState(guide.id);
@@ -2916,7 +3520,8 @@ var GuidelyClient = class {
2916
3520
  this.reportError(err);
2917
3521
  }
2918
3522
  }
2919
- showFlow(guide, manual, stepId) {
3523
+ showFlow(raw, manual, stepId) {
3524
+ const guide = this.localize(raw);
2920
3525
  const exclusive = isExclusiveGuide(guide);
2921
3526
  const flow = { guide, manual, view: null, controller: null };
2922
3527
  let renders = 0;
@@ -2931,7 +3536,7 @@ var GuidelyClient = class {
2931
3536
  const step = e.stepId ? guide.config.steps.find((s) => s.id === e.stepId) : void 0;
2932
3537
  this.enqueue(e.name, { guideId: guide.id, guideVersion: guide.version, stepId: e.stepId, stepIndex: e.stepIndex, properties: e.properties });
2933
3538
  if (e.name === "guide_started") {
2934
- this.recordImpression(guide);
3539
+ this.recordImpression(guide, manual);
2935
3540
  this.emitter.emit("guide:start", guide);
2936
3541
  } else if (e.name === "step_viewed" && step) {
2937
3542
  this.emitter.emit("guide:step", { guide, stepId: step.id, stepIndex: e.stepIndex ?? 0 });
@@ -2978,21 +3583,24 @@ var GuidelyClient = class {
2978
3583
  if (this.flows.get(guide.id) === flow) this.flows.delete(guide.id);
2979
3584
  }
2980
3585
  }
2981
- showChecklist(guide, open) {
3586
+ showChecklist(raw, open) {
3587
+ if (!raw.config.checklist || this.checklists.has(raw.id)) return;
3588
+ const guide = this.localize(raw);
2982
3589
  const config = guide.config.checklist;
2983
- if (!config || this.checklists.has(guide.id)) return;
3590
+ if (!config) return;
2984
3591
  const progress = this.getChecklistProgress(guide.id);
2985
3592
  const first = !this.state.guides[guide.id]?.impressions;
2986
- this.checklists.set(guide.id, { open: open ?? (config.autoOpen && first && !progress.completedAt) });
3593
+ this.checklists.set(guide.id, { guide, open: open ?? (config.autoOpen && first && !progress.completedAt) });
2987
3594
  if (first) this.recordImpression(guide);
2988
3595
  if (this.checklists.get(guide.id)?.open) this.enqueue("guide_viewed", { guideId: guide.id, guideVersion: guide.version });
2989
3596
  this.notify();
2990
3597
  }
2991
- showSurvey(guide, manual) {
2992
- if (!guide.config.survey) return;
3598
+ showSurvey(raw, manual) {
3599
+ if (!raw.config.survey) return;
3600
+ const guide = this.localize(raw);
2993
3601
  this.activeSurvey = { guide, manual };
2994
3602
  this.surveyView = { key: `${guide.id}:${randomId(6)}`, guide, submitted: false };
2995
- this.recordImpression(guide);
3603
+ this.recordImpression(guide, manual);
2996
3604
  this.enqueue("survey_displayed", { guideId: guide.id, guideVersion: guide.version });
2997
3605
  this.emitter.emit("guide:start", guide);
2998
3606
  this.notify();
@@ -3052,10 +3660,7 @@ var GuidelyClient = class {
3052
3660
  const steps = [];
3053
3661
  for (const f of [...this.flows.values(), ...this.active ? [this.active] : []]) if (f.view) steps.push({ ...f.view, expanded: this.expanded.has(f.guide.id) });
3054
3662
  const checklists = [];
3055
- this.checklists.forEach((w, id) => {
3056
- const guide = this.findGuide(id);
3057
- if (guide) checklists.push({ guide, progress: this.getChecklistProgress(id), open: w.open });
3058
- });
3663
+ this.checklists.forEach((w, id) => checklists.push({ guide: w.guide, progress: this.getChecklistProgress(id), open: w.open }));
3059
3664
  this.ui = { steps, checklists, survey: this.surveyView, resourceCenterOpen: this.resourceCenterOpen, theme: this.themeFor(), messenger: !!this.messenger };
3060
3665
  for (const cb of Array.from(this.uiListeners)) {
3061
3666
  try {
@@ -3140,10 +3745,58 @@ var GuidelyClient = class {
3140
3745
  getAudienceIds() {
3141
3746
  return this.state.audienceIds;
3142
3747
  }
3748
+ /** The published resource center in the visitor's language, or null when it is off. */
3143
3749
  getResourceCenter() {
3144
- const rc = this.config?.resourceCenter;
3750
+ const rc = this.localizedResourceCenter();
3145
3751
  return rc && rc.enabled ? rc : null;
3146
3752
  }
3753
+ /** The resource center in the visitor's language (cached until the config or language changes). */
3754
+ localizedResourceCenter() {
3755
+ const rc = this.config?.resourceCenter;
3756
+ if (!rc) return null;
3757
+ const locales = this.locales();
3758
+ const key = `${locales.join(",")}|${this.defaultLocale()}`;
3759
+ if (this.localizedRc?.source !== rc || this.localizedRc.locales !== key) this.localizedRc = { source: rc, locales: key, value: localizeResourceCenter(rc, locales, this.defaultLocale()) };
3760
+ return this.localizedRc.value;
3761
+ }
3762
+ /** Evaluates targeting rules (e.g. a resource-center section's) for the current visitor and screen. */
3763
+ matchesTargeting(rules) {
3764
+ if (!rules) return true;
3765
+ try {
3766
+ return evaluateRules(rules, this.getContext());
3767
+ } catch (err) {
3768
+ this.reportError(err);
3769
+ return false;
3770
+ }
3771
+ }
3772
+ /** Resource-center sections this visitor sees, in order (disabled and non-matching ones left out). */
3773
+ getResourceCenterModules() {
3774
+ const rc = this.getResourceCenter();
3775
+ if (!rc) return [];
3776
+ return orderedModules(rc.modules).filter((m) => m.enabled && (!("targeting" in m) || this.matchesTargeting(m.targeting))).map((m) => m.id);
3777
+ }
3778
+ /**
3779
+ * Sections of the messenger's Help tab, in order: the resource center's modules without "What's
3780
+ * new" (the messenger has its own News tab); every section when no resource center is published.
3781
+ */
3782
+ getHelpModules() {
3783
+ const modules = this.getResourceCenter() ? this.getResourceCenterModules() : [...RC_MODULE_IDS];
3784
+ return modules.filter((id) => id !== "announcements");
3785
+ }
3786
+ /** "What's new" entries this visitor sees (newest first); empty when the section is off. */
3787
+ getAnnouncements() {
3788
+ const rc = this.getResourceCenter();
3789
+ if (!rc || !this.getResourceCenterModules().includes("announcements")) return [];
3790
+ return visibleAnnouncements(rc.announcements, this.state.audienceIds);
3791
+ }
3792
+ /** "What's new" entries published since the visitor last opened the resource center. */
3793
+ getUnreadAnnouncements() {
3794
+ return unreadAnnouncements(this.getAnnouncements(), this.state.announcementsSeenAt);
3795
+ }
3796
+ /** Whether an entry is new to the visitor in the resource center that is open now. */
3797
+ isAnnouncementNew(a) {
3798
+ return unreadAnnouncements([a], this.announcementsSeenAtOpen) > 0;
3799
+ }
3147
3800
  async searchResourceCenter(q) {
3148
3801
  try {
3149
3802
  const res = await this.api.get("/resource-center/search", { params: { q } });
@@ -3152,6 +3805,11 @@ var GuidelyClient = class {
3152
3805
  return [];
3153
3806
  }
3154
3807
  }
3808
+ /** The language of the resource center as shown, when it isn't the default (sent with article fetches). */
3809
+ articleLocale() {
3810
+ const locale = this.localizedResourceCenter()?.locale;
3811
+ return locale && locale.toLowerCase() !== this.defaultLocale().toLowerCase() ? locale : void 0;
3812
+ }
3155
3813
  articleViewed(article) {
3156
3814
  this.enqueue("article_viewed", { properties: { articleId: article.id, slug: article.slug } });
3157
3815
  }
@@ -3286,6 +3944,11 @@ var GuidelyClient = class {
3286
3944
  }
3287
3945
  if (this.resourceCenterOpen) return;
3288
3946
  this.resourceCenterOpen = true;
3947
+ this.announcementsSeenAtOpen = this.state.announcementsSeenAt ?? null;
3948
+ if (this.getAnnouncements().length) {
3949
+ this.state.announcementsSeenAt = nowIso();
3950
+ this.persistState();
3951
+ }
3289
3952
  this.enqueue("resource_center_opened");
3290
3953
  this.emitter.emit("resource-center:open");
3291
3954
  this.notify();
@@ -3321,7 +3984,8 @@ var GuidelyClient = class {
3321
3984
  onMessage: (message, conversation) => this.emitter.emit("conversation:message", { message, conversation }),
3322
3985
  onStartGuide: (key) => this.setTimer(() => this.startGuide(key), 0),
3323
3986
  onError: (err) => this.log("messenger", err instanceof Error ? err.message : err),
3324
- health: this.api.health
3987
+ health: this.api.health,
3988
+ locale: () => this.articleLocale()
3325
3989
  });
3326
3990
  this.resourceCenterOpen = false;
3327
3991
  }
@@ -3335,6 +3999,14 @@ var GuidelyClient = class {
3335
3999
  getMessenger() {
3336
4000
  return this.messenger;
3337
4001
  }
4002
+ /** Where the visitor dragged the messenger bubble on this device, or null to use the configured spot. */
4003
+ getMessengerLauncherPlacement() {
4004
+ return parsePlacement(this.store.get(LAUNCHER_PLACEMENT_KEY));
4005
+ }
4006
+ /** Remembers the bubble's spot on this device (in memory only when no `storage` was passed). */
4007
+ setMessengerLauncherPlacement(placement) {
4008
+ this.store.set(LAUNCHER_PLACEMENT_KEY, serializePlacement(placement));
4009
+ }
3338
4010
  /** Opens the Scout messenger, optionally on a tab. No-op when Scout is off for this platform. */
3339
4011
  openMessenger(tab) {
3340
4012
  this.whenHydrated(() => this.messenger?.open(tab));
@@ -3365,6 +4037,11 @@ var GuidelyClient = class {
3365
4037
  onFlagsChange(cb) {
3366
4038
  return this.flags.subscribe(cb);
3367
4039
  }
4040
+ /** Device facts on every screen view, read by the Pages devices / browsers breakdowns. */
4041
+ deviceProperties() {
4042
+ const d = this.bridge.device;
4043
+ return { platform: d.platform, deviceType: d.deviceType, os: d.platform };
4044
+ }
3368
4045
  /** Attributes flags are evaluated with: identity, traits, account, audiences, screen and device. */
3369
4046
  flagContext() {
3370
4047
  const d = this.bridge.device;
@@ -3455,7 +4132,191 @@ var GuidelyClient = class {
3455
4132
  }
3456
4133
  };
3457
4134
  function forPlatform(config, platform) {
3458
- return config.guides.every((g) => supportsPlatform(g.config, platform)) ? config : { ...config, guides: config.guides.filter((g) => supportsPlatform(g.config, platform)) };
4135
+ const keep = (g) => supportsPlatform(g.config, platform);
4136
+ const holdouts = config.guideHoldouts?.filter((h) => keep(h.guide)).map((h) => ({ ...h, guide: withoutCode(h.guide) }));
4137
+ return { ...config, guides: config.guides.filter(keep).map(withoutCode), ...holdouts ? { guideHoldouts: holdouts } : {} };
4138
+ }
4139
+ function withoutCode(guide) {
4140
+ if (!guide.config.steps.some((s) => s.code)) return guide;
4141
+ return { ...guide, config: { ...guide.config, steps: guide.config.steps.map(({ code: _code, ...step }) => step) } };
4142
+ }
4143
+
4144
+ // src/ui/Autocapture.tsx
4145
+ var import_react8 = require("react");
4146
+ var import_react_native11 = require("react-native");
4147
+
4148
+ // src/core/viewTree.ts
4149
+ var HOST_TEXT = 6;
4150
+ var MAX_ANCESTORS = 500;
4151
+ var MAX_TEXT_NODES = 300;
4152
+ var INTERACTIVE_ROLES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem", "checkbox", "switch", "radio", "togglebutton", "imagebutton", "option", "combobox", "adjustable"]);
4153
+ var TEXT_INPUT_HOST = /TextInput/i;
4154
+ var NOISE = /^(Anonymous|Unknown|ForwardRef|Memo|Context|Provider|Consumer|Fragment|StrictMode|Suspense|Profiler)$|Context$|Provider$|Consumer$/;
4155
+ var isFiber = (v) => !!v && typeof v === "object" && "return" in v && "memoizedProps" in v;
4156
+ function fiberFromEvent(event) {
4157
+ try {
4158
+ const e = event;
4159
+ if (isFiber(e?._targetInst)) return e._targetInst;
4160
+ const target = e?.target;
4161
+ if (!target || typeof target !== "object") return null;
4162
+ for (const key of ["__internalInstanceHandle", "_internalInstanceHandle", "_internalFiberInstanceHandleDEV"]) if (isFiber(target[key])) return target[key];
4163
+ for (const sym of Object.getOwnPropertySymbols(target)) if (sym.description === "internalInstanceHandle" && isFiber(target[sym])) return target[sym];
4164
+ } catch {
4165
+ }
4166
+ return null;
4167
+ }
4168
+ function componentName(type, depth = 0) {
4169
+ if (!type || depth > 3) return null;
4170
+ if (typeof type === "function") {
4171
+ const f = type;
4172
+ const n = typeof f.displayName === "string" && f.displayName ? f.displayName : typeof f.name === "string" ? f.name : "";
4173
+ return n || null;
4174
+ }
4175
+ if (typeof type === "object") {
4176
+ const o = type;
4177
+ if (typeof o.displayName === "string" && o.displayName) return o.displayName;
4178
+ return componentName(o.render, depth + 1) ?? componentName(o.type, depth + 1);
4179
+ }
4180
+ return null;
4181
+ }
4182
+ var isHost = (f) => typeof f.type === "string";
4183
+ var isTextInput = (f) => typeof f.type === "string" ? TEXT_INPUT_HOST.test(f.type) : componentName(f.type) === "TextInput";
4184
+ function isInteractive(props) {
4185
+ if (!props || typeof props !== "object") return null;
4186
+ if (typeof props.onPress === "function" || typeof props.onLongPress === "function") return "press";
4187
+ const role = props.accessibilityRole ?? props.role;
4188
+ return typeof role === "string" && INTERACTIVE_ROLES.has(role) ? "role" : null;
4189
+ }
4190
+ function firstHost(f) {
4191
+ let cur = f;
4192
+ for (let i = 0; cur && i < 30; i++, cur = cur.child) if (isHost(cur)) return cur;
4193
+ return null;
4194
+ }
4195
+ function idSelector(f) {
4196
+ const host = firstHost(f);
4197
+ const sources = [f.memoizedProps, host?.memoizedProps];
4198
+ for (const key of ["nativeID", "testID"]) {
4199
+ for (const p of sources) {
4200
+ const v = p && typeof p === "object" ? p[key] : void 0;
4201
+ if (typeof v === "string" && v) return attrSelector(key, v);
4202
+ }
4203
+ }
4204
+ return null;
4205
+ }
4206
+ function pathSelector(f) {
4207
+ const parts = [];
4208
+ let cur = f;
4209
+ for (let i = 0; cur && parts.length < 4 && i < MAX_ANCESTORS; i++, cur = cur.return) {
4210
+ if (isHost(cur)) continue;
4211
+ const name = componentName(cur.type);
4212
+ if (!name || NOISE.test(name)) continue;
4213
+ let same = 0;
4214
+ let index = 0;
4215
+ for (let s = cur.return?.child, n = 0; s && n < 500; s = s.sibling, n++) {
4216
+ if (s.type !== cur.type) continue;
4217
+ same += 1;
4218
+ if (s === cur) index = same;
4219
+ }
4220
+ parts.unshift(same > 1 && index ? `${name}:nth-of-type(${index})` : name);
4221
+ }
4222
+ return parts.join(" > ").slice(0, 300) || "View";
4223
+ }
4224
+ function subtreeText(f, opts) {
4225
+ const out = [];
4226
+ let visited = 0;
4227
+ let masked = false;
4228
+ const walk = (node) => {
4229
+ for (let n = node; n && visited < MAX_TEXT_NODES && !masked; n = n.sibling) {
4230
+ visited += 1;
4231
+ if (opts.isMask?.(n.type)) {
4232
+ masked = true;
4233
+ return;
4234
+ }
4235
+ if (isTextInput(n)) continue;
4236
+ if (n.tag === HOST_TEXT && (typeof n.memoizedProps === "string" || typeof n.memoizedProps === "number")) out.push(String(n.memoizedProps));
4237
+ else walk(n.child);
4238
+ }
4239
+ };
4240
+ if (opts.isMask?.(f.type)) return { text: "", masked: true };
4241
+ walk(f.child);
4242
+ return { text: out.join(" "), masked };
4243
+ }
4244
+ function measureHost(host) {
4245
+ return new Promise((resolve) => {
4246
+ let settled = false;
4247
+ const done = (r) => {
4248
+ if (settled) return;
4249
+ settled = true;
4250
+ clearTimeout(timer);
4251
+ resolve(r);
4252
+ };
4253
+ const timer = setTimeout(() => done(null), 500);
4254
+ const cb = (x, y, width, height) => done(Number.isFinite(x) && Number.isFinite(y) && width > 0 && height > 0 ? { x, y, width, height } : null);
4255
+ try {
4256
+ const sn = host.stateNode;
4257
+ const pub = sn?.canonical?.publicInstance ?? sn;
4258
+ if (pub && typeof pub.measureInWindow === "function") return pub.measureInWindow(cb);
4259
+ const ui = globalThis.nativeFabricUIManager;
4260
+ if (sn?.node && typeof ui?.measureInWindow === "function") return ui.measureInWindow(sn.node, cb);
4261
+ } catch {
4262
+ }
4263
+ done(null);
4264
+ });
4265
+ }
4266
+ function describe(f, masked, opts, withText) {
4267
+ const props = f.memoizedProps && typeof f.memoizedProps === "object" ? f.memoizedProps : {};
4268
+ const host = firstHost(f);
4269
+ const el = { selector: idSelector(f) ?? pathSelector(f), tag: componentName(f.type) ?? "View" };
4270
+ if (withText && !masked) {
4271
+ const sub = subtreeText(f, opts);
4272
+ if (!sub.masked) {
4273
+ const hostProps = host?.memoizedProps && typeof host.memoizedProps === "object" ? host.memoizedProps : {};
4274
+ const label = props.accessibilityLabel ?? props["aria-label"] ?? hostProps.accessibilityLabel ?? hostProps["aria-label"];
4275
+ const text = cleanText(sub.text) ?? (typeof label === "string" ? cleanText(label) : void 0);
4276
+ if (text) el.text = text;
4277
+ }
4278
+ }
4279
+ if (host) el.measure = () => measureHost(host);
4280
+ return el;
4281
+ }
4282
+ function resolveFiber(target, opts = {}) {
4283
+ const none = { ignored: false, element: null, hit: null };
4284
+ if (!target) return none;
4285
+ try {
4286
+ let press = null;
4287
+ let role = null;
4288
+ let hitFiber = null;
4289
+ let stopped = false;
4290
+ let masked = false;
4291
+ for (let f = target, i = 0; f && i < MAX_ANCESTORS; f = f.return, i++) {
4292
+ if (opts.isIgnored?.(f.type)) return { ignored: true, element: null, hit: null };
4293
+ if (opts.isMask?.(f.type)) masked = true;
4294
+ if (stopped) continue;
4295
+ if (isTextInput(f)) {
4296
+ stopped = true;
4297
+ continue;
4298
+ }
4299
+ if (isHost(f)) continue;
4300
+ const name = componentName(f.type);
4301
+ if (!hitFiber && name && !NOISE.test(name)) hitFiber = f;
4302
+ const kind = isInteractive(f.memoizedProps);
4303
+ if (kind === "press") {
4304
+ press = f;
4305
+ stopped = true;
4306
+ } else if (kind === "role" && !role) role = f;
4307
+ }
4308
+ const element = press ?? role;
4309
+ return {
4310
+ ignored: false,
4311
+ element: element ? describe(element, masked, opts, true) : null,
4312
+ hit: hitFiber ? describe(hitFiber, masked, opts, componentName(hitFiber.type) === "Text") : null
4313
+ };
4314
+ } catch {
4315
+ return none;
4316
+ }
4317
+ }
4318
+ function resolveTapTarget(event, opts = {}) {
4319
+ return resolveFiber(fiberFromEvent(event), opts);
3459
4320
  }
3460
4321
 
3461
4322
  // src/ui/Overlay.tsx
@@ -4009,14 +4870,89 @@ function ago(iso) {
4009
4870
  if (s < 86400) return `${Math.floor(s / 3600)}h`;
4010
4871
  return `${Math.floor(s / 86400)}d`;
4011
4872
  }
4012
- function MessengerLauncher({ store, palette: p, style }) {
4873
+ var LAUNCHER_SIZE = 54;
4874
+ function useReduceMotion() {
4875
+ const [on, setOn] = (0, import_react3.useState)(false);
4876
+ (0, import_react3.useEffect)(() => {
4877
+ let live = true;
4878
+ import_react_native5.AccessibilityInfo.isReduceMotionEnabled?.().then((v) => live && setOn(v)).catch(() => {
4879
+ });
4880
+ const sub = import_react_native5.AccessibilityInfo.addEventListener?.("reduceMotionChanged", setOn);
4881
+ return () => {
4882
+ live = false;
4883
+ sub?.remove();
4884
+ };
4885
+ }, []);
4886
+ return on;
4887
+ }
4888
+ function MessengerLauncher({ store, client, palette: p, style, frame, defaultSide }) {
4013
4889
  const snap = useMessenger(store);
4890
+ const reduceMotion = useReduceMotion();
4891
+ const [placement, setPlacement] = (0, import_react3.useState)(() => client.getMessengerLauncherPlacement());
4892
+ const pan = (0, import_react3.useRef)(new import_react_native5.Animated.ValueXY()).current;
4893
+ const offset = (0, import_react3.useRef)({ x: 0, y: 0 });
4894
+ const dragging = (0, import_react3.useRef)(false);
4895
+ const live = (0, import_react3.useRef)({ frame, defaultSide, reduceMotion });
4896
+ live.current = { frame, defaultSide, reduceMotion };
4897
+ const { width, height, size, defaultBottom } = frame;
4898
+ const { top, bottom, left, right } = frame.insets;
4899
+ (0, import_react3.useLayoutEffect)(() => {
4900
+ if (dragging.current) return;
4901
+ const f = live.current.frame;
4902
+ const anchor = placementPoint(f, { side: defaultSide, fraction: 0 });
4903
+ const at = placement ? placementPoint(f, placement) : anchor;
4904
+ offset.current = { x: at.x - anchor.x, y: at.y - anchor.y };
4905
+ pan.setValue(offset.current);
4906
+ }, [pan, placement, defaultSide, width, height, size, defaultBottom, top, bottom, left, right]);
4907
+ const responder = (0, import_react3.useMemo)(() => {
4908
+ const moved = (g) => Math.hypot(g.dx, g.dy) > LAUNCHER_DRAG_THRESHOLD;
4909
+ const anchor = () => placementPoint(live.current.frame, { side: live.current.defaultSide, fraction: 0 });
4910
+ let start = { x: 0, y: 0 };
4911
+ const follow = (g) => {
4912
+ const a = anchor();
4913
+ const at = clampToSafeArea(live.current.frame, { x: a.x + start.x + g.dx, y: a.y + start.y + g.dy });
4914
+ return { x: at.x - a.x, y: at.y - a.y };
4915
+ };
4916
+ const drop = (g) => {
4917
+ const a = anchor();
4918
+ const d = follow(g);
4919
+ const next = snapPlacement(live.current.frame, { x: a.x + d.x, y: a.y + d.y });
4920
+ const at = placementPoint(live.current.frame, next);
4921
+ const to = { x: at.x - a.x, y: at.y - a.y };
4922
+ offset.current = to;
4923
+ client.setMessengerLauncherPlacement(next);
4924
+ const done = ({ finished }) => {
4925
+ if (finished) dragging.current = false;
4926
+ setPlacement(next);
4927
+ };
4928
+ if (live.current.reduceMotion) {
4929
+ pan.setValue(to);
4930
+ done({ finished: true });
4931
+ } else import_react_native5.Animated.timing(pan, { toValue: to, duration: 300, easing: import_react_native5.Easing.out(import_react_native5.Easing.cubic), useNativeDriver: true }).start(done);
4932
+ };
4933
+ return import_react_native5.PanResponder.create({
4934
+ // Taps stay with the Pressable; only a real drag is taken (which cancels its press).
4935
+ onMoveShouldSetPanResponderCapture: (_, g) => moved(g),
4936
+ onMoveShouldSetPanResponder: (_, g) => moved(g),
4937
+ onPanResponderGrant: () => {
4938
+ dragging.current = true;
4939
+ start = offset.current;
4940
+ pan.stopAnimation((v) => {
4941
+ if (Number.isFinite(v?.x) && Number.isFinite(v?.y)) start = v;
4942
+ });
4943
+ },
4944
+ onPanResponderMove: (_, g) => pan.setValue(follow(g)),
4945
+ onPanResponderRelease: (_, g) => drop(g),
4946
+ onPanResponderTerminate: (_, g) => drop(g),
4947
+ onPanResponderTerminationRequest: () => false
4948
+ });
4949
+ }, [client, pan]);
4014
4950
  if (snap.open) return null;
4015
4951
  const label = snap.unread ? `Open ${snap.config.name}, ${snap.unread} unread` : `Open ${snap.config.name}`;
4016
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress: () => store.open(), style: ({ pressed }) => [styles4.launcher, style, { backgroundColor: p.primary, opacity: pressed ? 0.85 : 1 }], children: [
4952
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Animated.View, { ...responder.panHandlers, style: [styles4.launcherWrap, style, { transform: pan.getTranslateTransform() }], children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress: () => !dragging.current && store.open(), style: ({ pressed }) => [styles4.launcher, { backgroundColor: p.primary, opacity: pressed ? 0.85 : 1 }], children: [
4017
4953
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.launcherIcon, { color: p.onPrimary }], children: "\u{1F4AC}" }),
4018
4954
  snap.unread ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.View, { style: styles4.badge, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: styles4.badgeText, children: snap.unread }) }) : null
4019
- ] });
4955
+ ] }) });
4020
4956
  }
4021
4957
  function MessengerSheet({ store, client, palette: p, topInset, bottomInset }) {
4022
4958
  const snap = useMessenger(store);
@@ -4215,25 +5151,41 @@ function Help({ store, client, palette: p }) {
4215
5151
  ] }, title);
4216
5152
  const audiences = client.getAudienceIds();
4217
5153
  const sections = resourceCenterSections(articles?.collections ?? [], articles?.articles ?? [], audiences);
4218
- const listed = results?.filter((a) => isArticleListed(articles?.collections ?? [], a, audiences));
5154
+ const modules = client.getHelpModules();
5155
+ const searchOn = modules.includes("search");
5156
+ const listed = searchOn ? results?.filter((a) => isArticleListed(articles?.collections ?? [], a, audiences)) : void 0;
5157
+ const checklists = (rc?.checklistGuideIds ?? []).map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g && g.kind === "checklist");
5158
+ const renderModule = (id) => {
5159
+ switch (id) {
5160
+ case "guides":
5161
+ return recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u25B6"))) : null;
5162
+ case "checklists":
5163
+ return checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u2611"))) : null;
5164
+ case "articles":
5165
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
5166
+ !articles ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.ActivityIndicator, { color: p.muted }) : null,
5167
+ sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => open(a)))))
5168
+ ] });
5169
+ case "faqs":
5170
+ return rc?.faqs.length ? section(
5171
+ "FAQ",
5172
+ rc.faqs.map((f) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.View, { style: [styles4.row, styles4.faq, { borderColor: p.border }], children: [
5173
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.Pressable, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles4.faqHead, children: [
5174
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.rowTitle, styles4.flex, { color: p.text }], children: f.question }),
5175
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
5176
+ ] }),
5177
+ faq === f.id ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
5178
+ ] }, f.id))
5179
+ ) : null;
5180
+ case "links":
5181
+ return rc?.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null;
5182
+ default:
5183
+ return null;
5184
+ }
5185
+ };
4219
5186
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.ScrollView, { contentContainerStyle: styles4.content, keyboardShouldPersistTaps: "handled", children: [
4220
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.TextInput, { value: q, onChangeText: setQ, placeholder: "Search articles", placeholderTextColor: p.muted, accessibilityLabel: "Search help articles", returnKeyType: "search", style: [styles4.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }] }),
4221
- listed ? section(`Results for \u201C${q.trim()}\u201D`, listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => open(a))) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.rowSub, { color: p.muted }], children: "No articles found." })) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
4222
- recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u25B6"))) : null,
4223
- !articles ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.ActivityIndicator, { color: p.muted }) : null,
4224
- sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => open(a))))),
4225
- rc?.faqs.length ? section(
4226
- "FAQ",
4227
- rc.faqs.map((f) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.View, { style: [styles4.row, styles4.faq, { borderColor: p.border }], children: [
4228
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.Pressable, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles4.faqHead, children: [
4229
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.rowTitle, styles4.flex, { color: p.text }], children: f.question }),
4230
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
4231
- ] }),
4232
- faq === f.id ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
4233
- ] }, f.id))
4234
- ) : null,
4235
- rc?.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null
4236
- ] })
5187
+ searchOn ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.TextInput, { value: q, onChangeText: setQ, placeholder: "Search articles", placeholderTextColor: p.muted, accessibilityLabel: "Search help articles", returnKeyType: "search", style: [styles4.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }] }) : null,
5188
+ listed ? section(`Results for \u201C${q.trim()}\u201D`, listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => open(a))) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.rowSub, { color: p.muted }], children: "No articles found." })) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: modules.filter((id) => id !== "search").map((id) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react3.Fragment, { children: renderModule(id) }, id)) })
4237
5189
  ] });
4238
5190
  }
4239
5191
  function Article({ article, palette: p, client }) {
@@ -4245,7 +5197,8 @@ function Article({ article, palette: p, client }) {
4245
5197
  }
4246
5198
  var styles4 = import_react_native5.StyleSheet.create({
4247
5199
  flex: { flex: 1 },
4248
- launcher: { position: "absolute", width: 54, height: 54, borderRadius: 27, alignItems: "center", justifyContent: "center", elevation: 8, shadowColor: "#000", shadowOpacity: 0.22, shadowRadius: 12, shadowOffset: { width: 0, height: 4 } },
5200
+ launcherWrap: { position: "absolute", width: LAUNCHER_SIZE, height: LAUNCHER_SIZE, elevation: 8 },
5201
+ launcher: { width: LAUNCHER_SIZE, height: LAUNCHER_SIZE, borderRadius: 27, alignItems: "center", justifyContent: "center", elevation: 8, shadowColor: "#000", shadowOpacity: 0.22, shadowRadius: 12, shadowOffset: { width: 0, height: 4 } },
4249
5202
  launcherIcon: { fontSize: 24 },
4250
5203
  badge: { position: "absolute", top: -2, right: -2, minWidth: 20, height: 20, borderRadius: 10, backgroundColor: "#dc2626", alignItems: "center", justifyContent: "center", paddingHorizontal: 5, borderWidth: 2, borderColor: "#ffffff" },
4251
5204
  badgeText: { color: "#ffffff", fontSize: 11, fontWeight: "800" },
@@ -4336,6 +5289,8 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4336
5289
  const checklists = (0, import_react4.useMemo)(() => (rc?.checklistGuideIds ?? []).map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g && g.kind === "checklist"), [rc, guides]);
4337
5290
  if (!rc) return null;
4338
5291
  const close = () => client.closeResourceCenter();
5292
+ const modules = client.getResourceCenterModules();
5293
+ const announcements = client.getAnnouncements();
4339
5294
  const openArticle = (a) => {
4340
5295
  if (a.externalUrl) return client.navigate(a.externalUrl);
4341
5296
  client.articleViewed(a);
@@ -4354,7 +5309,51 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4354
5309
  ] }, title);
4355
5310
  const audiences = client.getAudienceIds();
4356
5311
  const sections = resourceCenterSections(rc.collections, rc.articles, audiences);
4357
- const listed = results?.filter((a) => isArticleListed(rc.collections, a, audiences));
5312
+ const listed = modules.includes("search") ? results?.filter((a) => isArticleListed(rc.collections, a, audiences)) : void 0;
5313
+ const link = (label, onPress) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress, hitSlop: 8, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.link, { color: p.primary }], children: label }) }, label);
5314
+ const announcement2 = (a) => {
5315
+ const linked = a.articleId ? rc.articles.find((x) => x.id === a.articleId) : void 0;
5316
+ const isNew = client.isAnnouncementNew(a);
5317
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
5318
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: styles5.faqHead, children: [
5319
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { accessibilityRole: "header", style: [styles5.rowTitle, styles5.flex, { color: p.text, fontFamily: p.fontFamily }], children: a.title }),
5320
+ isNew ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.badge, { color: p.onPrimary, backgroundColor: p.primary }], children: "NEW" }) : null
5321
+ ] }),
5322
+ a.body ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RichText, { html: a.body, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null,
5323
+ a.guideId || linked || a.url ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: styles5.links, children: [
5324
+ a.guideId ? link("Show me", () => (close(), client.startGuide(a.guideId))) : null,
5325
+ linked ? link("Read more", () => openArticle(linked)) : null,
5326
+ a.url ? link(a.linkLabel || "Learn more", () => client.navigate(a.url)) : null
5327
+ ] }) : null
5328
+ ] }, a.id);
5329
+ };
5330
+ const renderModule = (id) => {
5331
+ switch (id) {
5332
+ case "announcements":
5333
+ return announcements.length ? section("What's new", announcements.map(announcement2)) : null;
5334
+ case "guides":
5335
+ return recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => (close(), client.startGuide(g.id)), "\u25B6"))) : null;
5336
+ case "checklists":
5337
+ return checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => (close(), client.openChecklist(g.id)), "\u2611"))) : null;
5338
+ case "articles":
5339
+ 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)))));
5340
+ case "faqs":
5341
+ return rc.faqs.length ? section(
5342
+ "FAQ",
5343
+ rc.faqs.map((f) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
5344
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.Pressable, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles5.faqHead, children: [
5345
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.rowTitle, styles5.flex, { color: p.text }], children: f.question }),
5346
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
5347
+ ] }),
5348
+ faq === f.id ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
5349
+ ] }, f.id))
5350
+ ) : null;
5351
+ case "links":
5352
+ return rc.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null;
5353
+ default:
5354
+ return null;
5355
+ }
5356
+ };
4358
5357
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Modal, { visible: open, animationType: "slide", presentationStyle: import_react_native6.Platform.OS === "ios" ? "pageSheet" : "fullScreen", onRequestClose: () => article ? setArticle(null) : close(), children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.sheet, { backgroundColor: p.background, paddingTop: import_react_native6.Platform.OS === "ios" ? 8 : topInset }], children: [
4359
5358
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.header, { borderColor: p.border }], children: [
4360
5359
  article ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Pressable, { accessibilityRole: "button", accessibilityLabel: "Back", onPress: () => setArticle(null), hitSlop: 10, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.back, { color: p.primary }], children: "\u2039 Back" }) }) : null,
@@ -4365,7 +5364,7 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4365
5364
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { accessibilityRole: "header", style: [styles5.articleTitle, { color: p.text, fontFamily: p.fontFamily }], children: article.title }),
4366
5365
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RichText, { html: article.body, palette: p, onLink: (u) => client.navigate(u) })
4367
5366
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.ScrollView, { contentContainerStyle: [styles5.content, { paddingBottom: bottomInset + 24 }], keyboardShouldPersistTaps: "handled", children: [
4368
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5367
+ modules.includes("search") ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4369
5368
  import_react_native6.TextInput,
4370
5369
  {
4371
5370
  value: query,
@@ -4377,26 +5376,11 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
4377
5376
  returnKeyType: "search",
4378
5377
  style: [styles5.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }]
4379
5378
  }
4380
- ),
5379
+ ) : null,
4381
5380
  listed ? section(
4382
5381
  `Results for \u201C${query.trim()}\u201D`,
4383
5382
  listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => openArticle(a))) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.rowSub, { color: p.muted }], children: "No articles found." })
4384
- ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
4385
- recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => (close(), client.startGuide(g.id)), "\u25B6"))) : null,
4386
- checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => (close(), client.openChecklist(g.id)), "\u2611"))) : null,
4387
- sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => openArticle(a))))),
4388
- rc.faqs.length ? section(
4389
- "FAQ",
4390
- rc.faqs.map((f) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
4391
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.Pressable, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles5.faqHead, children: [
4392
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.rowTitle, styles5.flex, { color: p.text }], children: f.question }),
4393
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
4394
- ] }),
4395
- faq === f.id ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
4396
- ] }, f.id))
4397
- ) : null,
4398
- rc.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null
4399
- ] })
5383
+ ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: modules.filter((id) => id !== "search").map((id) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react4.Fragment, { children: renderModule(id) }, id)) })
4400
5384
  ] })
4401
5385
  ] }) });
4402
5386
  }
@@ -4416,7 +5400,10 @@ var styles5 = import_react_native6.StyleSheet.create({
4416
5400
  chev: { fontSize: 18 },
4417
5401
  faq: { flexDirection: "column", alignItems: "stretch" },
4418
5402
  faqHead: { flexDirection: "row", alignItems: "center", gap: 10 },
4419
- articleTitle: { fontSize: 22, fontWeight: "700" }
5403
+ articleTitle: { fontSize: 22, fontWeight: "700" },
5404
+ badge: { fontSize: 10, fontWeight: "800", letterSpacing: 0.5, paddingHorizontal: 6, paddingVertical: 2, borderRadius: 6, overflow: "hidden" },
5405
+ links: { flexDirection: "row", flexWrap: "wrap", gap: 16, marginTop: 4 },
5406
+ link: { fontSize: 14, fontWeight: "600" }
4420
5407
  });
4421
5408
 
4422
5409
  // src/ui/StepLayer.tsx
@@ -4444,25 +5431,28 @@ function positionCard(target, size, viewport, insets, placement, offset) {
4444
5431
  const order = rawSide === "auto" ? ["bottom", "top"] : [rawSide, opposite[rawSide], "bottom", "top"];
4445
5432
  const space = { top: target.y - insets.top, bottom: viewport.height - insets.bottom - (target.y + target.height) };
4446
5433
  const side = order.find(fits) ?? (space.bottom >= space.top ? "bottom" : "top");
4447
- const clamp2 = (v, min, max) => Math.max(min, Math.min(max, v));
5434
+ const clamp3 = (v, min, max) => Math.max(min, Math.min(max, v));
4448
5435
  if (side === "top" || side === "bottom") {
4449
5436
  const cx = target.x + target.width / 2;
4450
5437
  let x2 = align === "start" ? target.x : align === "end" ? target.x + target.width - size.width : cx - size.width / 2;
4451
- x2 = clamp2(x2, insets.left + MARGIN, Math.max(insets.left + MARGIN, viewport.width - insets.right - MARGIN - size.width));
5438
+ x2 = clamp3(x2, insets.left + MARGIN, Math.max(insets.left + MARGIN, viewport.width - insets.right - MARGIN - size.width));
4452
5439
  let y2 = side === "bottom" ? target.y + target.height + offset : target.y - offset - size.height;
4453
- y2 = clamp2(y2, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
4454
- return { x: x2, y: y2, side, arrow: clamp2(cx - x2, ARROW_INSET, size.width - ARROW_INSET) };
5440
+ y2 = clamp3(y2, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
5441
+ return { x: x2, y: y2, side, arrow: clamp3(cx - x2, ARROW_INSET, size.width - ARROW_INSET) };
4455
5442
  }
4456
5443
  const cy = target.y + target.height / 2;
4457
5444
  let y = align === "start" ? target.y : align === "end" ? target.y + target.height - size.height : cy - size.height / 2;
4458
- y = clamp2(y, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
5445
+ y = clamp3(y, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
4459
5446
  const x = side === "right" ? target.x + target.width + offset : target.x - offset - size.width;
4460
- return { x, y, side, arrow: clamp2(cy - y, ARROW_INSET, size.height - ARROW_INSET) };
5447
+ return { x, y, side, arrow: clamp3(cy - y, ARROW_INSET, size.height - ARROW_INSET) };
4461
5448
  }
4462
5449
 
4463
5450
  // src/ui/StepCard.tsx
4464
5451
  var import_react_native7 = require("react-native");
4465
- var import_jsx_runtime7 = require("react/jsx-runtime");
5452
+ var import_jsx_runtime7 = (
5453
+ // An empty description marks the image as decorative: screen readers skip it.
5454
+ require("react/jsx-runtime")
5455
+ );
4466
5456
  function StepCard({ view, client, palette: p, width, style, compact }) {
4467
5457
  const { step, guide } = view;
4468
5458
  const buttons = visibleButtons(step, view.canGoBack);
@@ -4470,7 +5460,16 @@ function StepCard({ view, client, palette: p, width, style, compact }) {
4470
5460
  const showProgress = step.showProgress && view.total > 1;
4471
5461
  const onLink = (url) => client.navigate(url);
4472
5462
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Surface, { palette: p, style: [{ width }, style], children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { accessible: false, accessibilityViewIsModal: view.asModal || step.type === "modal", style: compact ? styles6.compact : styles6.inner, children: [
4473
- step.imageUrl ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Image, { source: { uri: step.imageUrl }, style: [styles6.image, { borderRadius: Math.max(0, p.radius - 4) }], resizeMode: "cover", accessibilityIgnoresInvertColors: true }) : null,
5463
+ step.imageUrl ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
5464
+ import_react_native7.Image,
5465
+ {
5466
+ source: { uri: step.imageUrl },
5467
+ style: [styles6.image, { borderRadius: Math.max(0, p.radius - 4) }],
5468
+ resizeMode: "cover",
5469
+ accessibilityIgnoresInvertColors: true,
5470
+ ...step.imageAlt?.trim() ? { accessible: true, accessibilityRole: "image", accessibilityLabel: step.imageAlt.trim() } : { accessible: false, importantForAccessibility: "no" }
5471
+ }
5472
+ ) : null,
4474
5473
  step.title ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: styles6.header, children: [
4475
5474
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { accessibilityRole: "header", style: [styles6.title, { color: p.text, fontFamily: p.fontFamily }], children: step.title }),
4476
5475
  step.showClose ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CloseButton, { palette: p, onPress: () => client.closeStep(guide.id), label: "Dismiss guide" }) : null
@@ -5058,21 +6057,36 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
5058
6057
  const basePalette = palette(ui.theme, scheme);
5059
6058
  const messenger = ui.messenger ? client.getMessenger() : null;
5060
6059
  const showRcLauncher = !messenger && !!client.options.showResourceCenterLauncher && !!client.getResourceCenter();
6060
+ const unread = showRcLauncher ? client.getUnreadAnnouncements() : 0;
5061
6061
  const messengerLeft = messenger?.getSnapshot().config.launcher.position === "bottom-left";
5062
6062
  const checklistOnSameSide = ui.checklists.some((c) => c.guide.config.checklist?.position === "bottom-left" === messengerLeft) && client.options.showChecklistLauncher !== false;
6063
+ const messengerBottom = insets.bottom + launcherOffset + (checklistOnSameSide ? 56 : 0);
5063
6064
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native10.View, { ref, collapsable: false, pointerEvents: "box-none", style: import_react_native10.StyleSheet.absoluteFill, onLayout: measureOrigin, children: [
5064
6065
  ui.checklists.map((c) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Checklist, { view: c, client, palette: palette(client.themeFor(c.guide), scheme), geometry, showLauncher: client.options.showChecklistLauncher !== false, launcherBottom: launcherOffset }, c.guide.id)),
5065
- showRcLauncher ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
6066
+ showRcLauncher ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
5066
6067
  import_react_native10.Pressable,
5067
6068
  {
5068
6069
  accessibilityRole: "button",
5069
- accessibilityLabel: client.getResourceCenter()?.launcher.label || "Help",
6070
+ accessibilityLabel: rcLauncherLabel(client),
5070
6071
  onPress: () => client.openResourceCenter(),
5071
6072
  style: [styles9.rc, client.getResourceCenter()?.launcher.position === "bottom-left" ? { left: insets.left + 16 } : { right: insets.right + 16 }, { bottom: insets.bottom + launcherOffset + (ui.checklists.length ? 56 : 0), backgroundColor: basePalette.primary }],
5072
- children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native10.Text, { style: [styles9.rcText, { color: basePalette.onPrimary }], children: "?" })
6073
+ children: [
6074
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native10.Text, { style: [styles9.rcText, { color: basePalette.onPrimary }], children: "?" }),
6075
+ unread ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native10.View, { pointerEvents: "none", style: [styles9.rcBadge, { borderColor: basePalette.primary }], children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native10.Text, { style: styles9.rcBadgeText, children: unread > 9 ? "9+" : unread }) }) : null
6076
+ ]
6077
+ }
6078
+ ) : null,
6079
+ messenger && client.options.showMessengerLauncher !== false ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
6080
+ MessengerLauncher,
6081
+ {
6082
+ store: messenger,
6083
+ client,
6084
+ palette: basePalette,
6085
+ style: [messengerLeft ? { left: insets.left + 16 } : { right: insets.right + 16 }, { bottom: messengerBottom }],
6086
+ frame: { width, height, insets, size: LAUNCHER_SIZE, defaultBottom: messengerBottom },
6087
+ defaultSide: messengerLeft ? "left" : "right"
5073
6088
  }
5074
6089
  ) : null,
5075
- messenger && client.options.showMessengerLauncher !== false ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MessengerLauncher, { store: messenger, palette: basePalette, style: [messengerLeft ? { left: insets.left + 16 } : { right: insets.right + 16 }, { bottom: insets.bottom + launcherOffset + (checklistOnSameSide ? 56 : 0) }] }) : null,
5076
6090
  ui.survey ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Survey, { view: ui.survey, client, palette: palette(client.themeFor(ui.survey.guide), scheme), geometry }) : null,
5077
6091
  ui.steps.map((s) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(StepLayer, { view: s, client, palette: palette(client.themeFor(s.guide), scheme, s.guide.config.appearance), geometry }, s.key)),
5078
6092
  messenger ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MessengerSheet, { store: messenger, client, palette: basePalette, topInset: insets.top, bottomInset: insets.bottom }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ResourceCenter, { client, palette: basePalette, open: ui.resourceCenterOpen, topInset: insets.top, bottomInset: insets.bottom })
@@ -5080,28 +6094,76 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
5080
6094
  }
5081
6095
  var styles9 = import_react_native10.StyleSheet.create({
5082
6096
  rc: { position: "absolute", width: 48, height: 48, borderRadius: 24, alignItems: "center", justifyContent: "center", elevation: 8, shadowColor: "#000", shadowOpacity: 0.2, shadowRadius: 10, shadowOffset: { width: 0, height: 4 } },
5083
- rcText: { fontSize: 22, fontWeight: "800" }
6097
+ rcText: { fontSize: 22, fontWeight: "800" },
6098
+ rcBadge: { position: "absolute", top: -4, right: -4, minWidth: 20, height: 20, paddingHorizontal: 5, borderRadius: 10, borderWidth: 2, backgroundColor: "#dc2626", alignItems: "center", justifyContent: "center" },
6099
+ rcBadgeText: { color: "#ffffff", fontSize: 11, fontWeight: "800" }
5084
6100
  });
6101
+ function rcLauncherLabel(client) {
6102
+ const label = client.getResourceCenter()?.launcher.label || "Help";
6103
+ const unread = client.getUnreadAnnouncements();
6104
+ return unread ? `${label} (${unread} new update${unread === 1 ? "" : "s"})` : label;
6105
+ }
5085
6106
 
5086
- // src/ui/Target.tsx
5087
- var import_react8 = require("react");
5088
- var import_react_native11 = require("react-native");
6107
+ // src/ui/Autocapture.tsx
5089
6108
  var import_jsx_runtime11 = require("react/jsx-runtime");
5090
- var GuidelyContext = (0, import_react8.createContext)(null);
5091
- var GuidelyScreenContext = (0, import_react8.createContext)(null);
6109
+ function GuidelyMask({ children, ...rest }) {
6110
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native11.View, { ...rest, children });
6111
+ }
6112
+ var RESOLVE = {
6113
+ isIgnored: (type) => type === GuidelyOverlay,
6114
+ isMask: (type) => type === GuidelyMask
6115
+ };
6116
+ function useTapObserver(client) {
6117
+ const clientRef = (0, import_react8.useRef)(client);
6118
+ clientRef.current = client;
6119
+ return (0, import_react8.useMemo)(() => {
6120
+ const tracker = new TapTracker();
6121
+ const guard = (fn) => (e) => {
6122
+ try {
6123
+ fn(e);
6124
+ } catch {
6125
+ tracker.cancel();
6126
+ }
6127
+ };
6128
+ return {
6129
+ onTouchStartCapture: guard((e) => {
6130
+ const n = e.nativeEvent;
6131
+ if (!clientRef.current?.isTapCaptureOn()) return tracker.cancel();
6132
+ tracker.begin(n.pageX, n.pageY, Date.now(), Array.isArray(n.touches) ? n.touches.length : 1, n.identifier);
6133
+ }),
6134
+ onTouchMoveCapture: guard((e) => tracker.move(e.nativeEvent.pageX, e.nativeEvent.pageY, e.nativeEvent.identifier)),
6135
+ onTouchEndCapture: guard((e) => {
6136
+ const n = e.nativeEvent;
6137
+ const tap = tracker.end(n.pageX, n.pageY, Date.now(), n.identifier);
6138
+ const c = clientRef.current;
6139
+ if (!tap || !c) return;
6140
+ const r = resolveTapTarget(e, RESOLVE);
6141
+ if (!r.ignored) c.recordTap({ x: tap.x, y: tap.y, t: Date.now(), element: r.element, hit: r.hit });
6142
+ }),
6143
+ onTouchCancelCapture: guard(() => tracker.cancel())
6144
+ };
6145
+ }, []);
6146
+ }
6147
+
6148
+ // src/ui/Target.tsx
6149
+ var import_react9 = require("react");
6150
+ var import_react_native12 = require("react-native");
6151
+ var import_jsx_runtime12 = require("react/jsx-runtime");
6152
+ var GuidelyContext = (0, import_react9.createContext)(null);
6153
+ var GuidelyScreenContext = (0, import_react9.createContext)(null);
5092
6154
  function GuidelyScreenScope({ name, children }) {
5093
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(GuidelyScreenContext.Provider, { value: name, children });
6155
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(GuidelyScreenContext.Provider, { value: name, children });
5094
6156
  }
5095
6157
  function guidelyScreenLayout({ route, children }) {
5096
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(GuidelyScreenScope, { name: route.name, children });
6158
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(GuidelyScreenScope, { name: route.name, children });
5097
6159
  }
5098
- var TAP_SLOP = 10;
6160
+ var TAP_SLOP2 = 10;
5099
6161
  function useGuidelyTarget(id) {
5100
- const client = (0, import_react8.useContext)(GuidelyContext);
5101
- const screen = (0, import_react8.useContext)(GuidelyScreenContext);
5102
- const ref = (0, import_react8.useRef)(null);
5103
- const start = (0, import_react8.useRef)(null);
5104
- (0, import_react8.useEffect)(() => {
6162
+ const client = (0, import_react9.useContext)(GuidelyContext);
6163
+ const screen = (0, import_react9.useContext)(GuidelyScreenContext);
6164
+ const ref = (0, import_react9.useRef)(null);
6165
+ const start = (0, import_react9.useRef)(null);
6166
+ (0, import_react9.useEffect)(() => {
5105
6167
  if (!client || !id) return;
5106
6168
  return client.targets.register(id, {
5107
6169
  screen,
@@ -5112,15 +6174,15 @@ function useGuidelyTarget(id) {
5112
6174
  })
5113
6175
  });
5114
6176
  }, [client, id, screen]);
5115
- const onLayout = (0, import_react8.useCallback)(() => client?.targets.changed(), [client]);
5116
- const onTouchStart = (0, import_react8.useCallback)((e) => {
6177
+ const onLayout = (0, import_react9.useCallback)(() => client?.targets.changed(), [client]);
6178
+ const onTouchStart = (0, import_react9.useCallback)((e) => {
5117
6179
  start.current = { x: e.nativeEvent.pageX, y: e.nativeEvent.pageY };
5118
6180
  }, []);
5119
- const onTouchEnd = (0, import_react8.useCallback)(
6181
+ const onTouchEnd = (0, import_react9.useCallback)(
5120
6182
  (e) => {
5121
6183
  const s = start.current;
5122
6184
  start.current = null;
5123
- if (s && Math.abs(e.nativeEvent.pageX - s.x) < TAP_SLOP && Math.abs(e.nativeEvent.pageY - s.y) < TAP_SLOP) client?.targets.tapped(id);
6185
+ if (s && Math.abs(e.nativeEvent.pageX - s.x) < TAP_SLOP2 && Math.abs(e.nativeEvent.pageY - s.y) < TAP_SLOP2) client?.targets.tapped(id);
5124
6186
  },
5125
6187
  [client, id]
5126
6188
  );
@@ -5128,8 +6190,8 @@ function useGuidelyTarget(id) {
5128
6190
  }
5129
6191
  function GuidelyTarget({ id, children, onLayout, onTouchStart, onTouchEnd, ...rest }) {
5130
6192
  const t = useGuidelyTarget(id);
5131
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
5132
- import_react_native11.View,
6193
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
6194
+ import_react_native12.View,
5133
6195
  {
5134
6196
  ...rest,
5135
6197
  ref: t.ref,
@@ -5153,7 +6215,7 @@ function GuidelyTarget({ id, children, onLayout, onTouchStart, onTouchEnd, ...re
5153
6215
 
5154
6216
  // src/index.tsx
5155
6217
  var import_flags6 = require("@guidelytech/flags");
5156
- var import_jsx_runtime12 = require("react/jsx-runtime");
6218
+ var import_jsx_runtime13 = require("react/jsx-runtime");
5157
6219
  function deviceLocale() {
5158
6220
  try {
5159
6221
  return Intl.DateTimeFormat().resolvedOptions().locale;
@@ -5162,33 +6224,34 @@ function deviceLocale() {
5162
6224
  }
5163
6225
  }
5164
6226
  function defaultBridge(appVersion) {
5165
- const { width, height } = import_react_native12.Dimensions.get("window");
6227
+ const { width, height } = import_react_native13.Dimensions.get("window");
5166
6228
  return {
5167
6229
  device: {
5168
- platform: import_react_native12.Platform.OS === "android" ? "android" : "ios",
5169
- deviceType: import_react_native12.Platform.OS === "ios" && import_react_native12.Platform.isPad || Math.min(width, height) >= 600 ? "tablet" : "mobile",
6230
+ platform: import_react_native13.Platform.OS === "android" ? "android" : "ios",
6231
+ deviceType: import_react_native13.Platform.OS === "ios" && import_react_native13.Platform.isPad || Math.min(width, height) >= 600 ? "tablet" : "mobile",
5170
6232
  locale: deviceLocale(),
5171
6233
  appVersion
5172
6234
  },
5173
- openExternal: (url) => void import_react_native12.Linking.openURL(url).catch(() => {
5174
- })
6235
+ openExternal: (url) => void import_react_native13.Linking.openURL(url).catch(() => {
6236
+ }),
6237
+ windowSize: () => import_react_native13.Dimensions.get("window")
5175
6238
  };
5176
6239
  }
5177
6240
  function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPayload, flagAttributes, ...options }) {
5178
- const [client, setClient] = (0, import_react9.useState)(null);
5179
- const optionsRef = (0, import_react9.useRef)(options);
6241
+ const [client, setClient] = (0, import_react10.useState)(null);
6242
+ const optionsRef = (0, import_react10.useRef)(options);
5180
6243
  optionsRef.current = options;
5181
- const userRef = (0, import_react9.useRef)(user);
6244
+ const userRef = (0, import_react10.useRef)(user);
5182
6245
  userRef.current = user;
5183
6246
  const flagsOpts = options.flags === false ? null : options.flags ?? {};
5184
6247
  const payload = flagsPayload ?? flagsOpts?.payload ?? null;
5185
- const payloadRef = (0, import_react9.useRef)(payload);
6248
+ const payloadRef = (0, import_react10.useRef)(payload);
5186
6249
  payloadRef.current = payload;
5187
- const appAttributes = (0, import_react9.useMemo)(() => ({ ...flagsOpts?.attributes ?? {}, ...flagAttributes ?? {} }), [(0, import_flags3.stableStringify)(flagsOpts?.attributes ?? {}), (0, import_flags3.stableStringify)(flagAttributes ?? {})]);
5188
- const appAttributesRef = (0, import_react9.useRef)(appAttributes);
6250
+ const appAttributes = (0, import_react10.useMemo)(() => ({ ...flagsOpts?.attributes ?? {}, ...flagAttributes ?? {} }), [(0, import_flags3.stableStringify)(flagsOpts?.attributes ?? {}), (0, import_flags3.stableStringify)(flagAttributes ?? {})]);
6251
+ const appAttributesRef = (0, import_react10.useRef)(appAttributes);
5189
6252
  appAttributesRef.current = appAttributes;
5190
6253
  const { projectId, environment, apiUrl, storage } = options;
5191
- (0, import_react9.useEffect)(() => {
6254
+ (0, import_react10.useEffect)(() => {
5192
6255
  if (!projectId) return;
5193
6256
  let c;
5194
6257
  try {
@@ -5214,7 +6277,7 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
5214
6277
  }
5215
6278
  setClient(c);
5216
6279
  void c.init();
5217
- const sub = import_react_native12.AppState.addEventListener("change", (s) => c.setAppState(s));
6280
+ const sub = import_react_native13.AppState.addEventListener("change", (s) => c.setAppState(s));
5218
6281
  return () => {
5219
6282
  sub.remove();
5220
6283
  c.destroy();
@@ -5222,8 +6285,8 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
5222
6285
  };
5223
6286
  }, [projectId, environment, apiUrl, storage]);
5224
6287
  const userKey = user ? JSON.stringify([user.id, user.traits ?? null, user.account ?? null]) : "";
5225
- const hadUser = (0, import_react9.useRef)(false);
5226
- (0, import_react9.useEffect)(() => {
6288
+ const hadUser = (0, import_react10.useRef)(false);
6289
+ (0, import_react10.useEffect)(() => {
5227
6290
  if (!client) return;
5228
6291
  if (user && user.id) {
5229
6292
  hadUser.current = true;
@@ -5233,16 +6296,21 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
5233
6296
  client.reset();
5234
6297
  }
5235
6298
  }, [client, userKey]);
5236
- (0, import_react9.useEffect)(() => {
6299
+ (0, import_react10.useEffect)(() => {
5237
6300
  client?.flags.setAttributes(appAttributes);
5238
6301
  }, [client, appAttributes]);
6302
+ (0, import_react10.useEffect)(() => {
6303
+ client?.setLocale(options.locale ?? null);
6304
+ }, [client, options.locale]);
5239
6305
  const bootstrap = useBootstrap(payload, { ...user?.id ? { ...user.traits ?? {}, id: user.id } : {}, ...appAttributes });
5240
- const flags = (0, import_react9.useMemo)(() => ({ client: client?.flags ?? null, bootstrap }), [client, bootstrap]);
5241
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(GuidelyContext.Provider, { value: client, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(FlagsContext.Provider, { value: flags, children: [
5242
- children,
5243
- client ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(GuidelyOverlay, { client, insets, launcherOffset }) : null
6306
+ const flags = (0, import_react10.useMemo)(() => ({ client: client?.flags ?? null, bootstrap }), [client, bootstrap]);
6307
+ const tapObserver = useTapObserver(client);
6308
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(GuidelyContext.Provider, { value: client, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(FlagsContext.Provider, { value: flags, children: [
6309
+ options.autocapture === false ? children : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native13.View, { style: styles10.root, ...tapObserver, children }),
6310
+ client ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(GuidelyOverlay, { client, insets, launcherOffset }) : null
5244
6311
  ] }) });
5245
6312
  }
6313
+ var styles10 = import_react_native13.StyleSheet.create({ root: { flex: 1 } });
5246
6314
  var NOOP_RESULTS = {
5247
6315
  init: Promise.resolve(),
5248
6316
  ready: Promise.resolve(),
@@ -5291,12 +6359,12 @@ var noopClient = new Proxy({}, {
5291
6359
  }
5292
6360
  });
5293
6361
  function useGuidely() {
5294
- return (0, import_react9.useContext)(GuidelyContext) ?? noopClient;
6362
+ return (0, import_react10.useContext)(GuidelyContext) ?? noopClient;
5295
6363
  }
5296
6364
  function useGuidelyReady() {
5297
- const client = (0, import_react9.useContext)(GuidelyContext);
5298
- const [ready, setReady] = (0, import_react9.useState)(() => !!client?.isClientReady());
5299
- (0, import_react9.useEffect)(() => {
6365
+ const client = (0, import_react10.useContext)(GuidelyContext);
6366
+ const [ready, setReady] = (0, import_react10.useState)(() => !!client?.isClientReady());
6367
+ (0, import_react10.useEffect)(() => {
5300
6368
  if (!client) return setReady(false);
5301
6369
  setReady(client.isClientReady());
5302
6370
  const off = client.on("ready", () => setReady(true));
@@ -5306,44 +6374,44 @@ function useGuidelyReady() {
5306
6374
  return ready;
5307
6375
  }
5308
6376
  function useGuidelyIdentify(userId, traits, account) {
5309
- const client = (0, import_react9.useContext)(GuidelyContext);
6377
+ const client = (0, import_react10.useContext)(GuidelyContext);
5310
6378
  const key = JSON.stringify([userId ?? null, traits ?? null, account ?? null]);
5311
- (0, import_react9.useEffect)(() => {
6379
+ (0, import_react10.useEffect)(() => {
5312
6380
  if (client && userId) void client.identify(userId, traits, account);
5313
6381
  }, [client, key]);
5314
6382
  }
5315
6383
  function useGuidelyEvent(event, handler) {
5316
- const client = (0, import_react9.useContext)(GuidelyContext);
5317
- const ref = (0, import_react9.useRef)(handler);
6384
+ const client = (0, import_react10.useContext)(GuidelyContext);
6385
+ const ref = (0, import_react10.useRef)(handler);
5318
6386
  ref.current = handler;
5319
- (0, import_react9.useEffect)(() => {
6387
+ (0, import_react10.useEffect)(() => {
5320
6388
  if (!client) return;
5321
6389
  return client.on(event, ((...args) => ref.current(...args)));
5322
6390
  }, [client, event]);
5323
6391
  }
5324
6392
  function useGuidelyScreen(name, properties) {
5325
- const client = (0, import_react9.useContext)(GuidelyContext);
5326
- (0, import_react9.useEffect)(() => {
6393
+ const client = (0, import_react10.useContext)(GuidelyContext);
6394
+ (0, import_react10.useEffect)(() => {
5327
6395
  if (client && name) client.screen(name, properties);
5328
6396
  }, [client, name]);
5329
6397
  }
5330
6398
  function useGuidelyNavigation(navigationRef, options = {}) {
5331
- const client = (0, import_react9.useContext)(GuidelyContext);
5332
- const nameRef = (0, import_react9.useRef)(options.screenName);
6399
+ const client = (0, import_react10.useContext)(GuidelyContext);
6400
+ const nameRef = (0, import_react10.useRef)(options.screenName);
5333
6401
  nameRef.current = options.screenName;
5334
6402
  const report = () => {
5335
6403
  const route = navigationRef.getCurrentRoute?.();
5336
6404
  if (client && route?.name) client.screen(nameRef.current ? nameRef.current(route) : route.name);
5337
6405
  };
5338
- (0, import_react9.useEffect)(report, [client]);
6406
+ (0, import_react10.useEffect)(report, [client]);
5339
6407
  return { onReady: report, onStateChange: report };
5340
6408
  }
5341
6409
  function useUiSelector(select, fallback) {
5342
- const client = (0, import_react9.useContext)(GuidelyContext);
5343
- const selectRef = (0, import_react9.useRef)(select);
6410
+ const client = (0, import_react10.useContext)(GuidelyContext);
6411
+ const selectRef = (0, import_react10.useRef)(select);
5344
6412
  selectRef.current = select;
5345
- const [value, setValue] = (0, import_react9.useState)(() => client ? select(client) : fallback);
5346
- (0, import_react9.useEffect)(() => {
6413
+ const [value, setValue] = (0, import_react10.useState)(() => client ? select(client) : fallback);
6414
+ (0, import_react10.useEffect)(() => {
5347
6415
  if (!client) return;
5348
6416
  let last = JSON.stringify(selectRef.current(client));
5349
6417
  setValue(selectRef.current(client));
@@ -5383,14 +6451,18 @@ function useActiveGuide() {
5383
6451
  FeatureValue,
5384
6452
  FlagsProvider,
5385
6453
  GuidelyClient,
6454
+ GuidelyMask,
5386
6455
  GuidelyOverlay,
5387
6456
  GuidelyProvider,
5388
6457
  GuidelyScreenScope,
5389
6458
  GuidelyTarget,
6459
+ HeatmapBuffer,
5390
6460
  InMemoryStickyBucketStore,
5391
6461
  Messenger,
6462
+ RageClickDetector,
5392
6463
  SDK_NAME,
5393
6464
  SDK_VERSION,
6465
+ TapTracker,
5394
6466
  TargetRegistry,
5395
6467
  createFlagsClient,
5396
6468
  createSseParser,