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