@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.cjs
CHANGED
|
@@ -28,14 +28,18 @@ __export(index_exports, {
|
|
|
28
28
|
FeatureValue: () => FeatureValue,
|
|
29
29
|
FlagsProvider: () => FlagsProvider,
|
|
30
30
|
GuidelyClient: () => GuidelyClient,
|
|
31
|
+
GuidelyMask: () => GuidelyMask,
|
|
31
32
|
GuidelyOverlay: () => GuidelyOverlay,
|
|
32
33
|
GuidelyProvider: () => GuidelyProvider,
|
|
33
34
|
GuidelyScreenScope: () => GuidelyScreenScope,
|
|
34
35
|
GuidelyTarget: () => GuidelyTarget,
|
|
36
|
+
HeatmapBuffer: () => HeatmapBuffer,
|
|
35
37
|
InMemoryStickyBucketStore: () => import_flags6.InMemoryStickyBucketStore,
|
|
36
38
|
Messenger: () => Messenger,
|
|
39
|
+
RageClickDetector: () => RageClickDetector,
|
|
37
40
|
SDK_NAME: () => SDK_NAME,
|
|
38
41
|
SDK_VERSION: () => SDK_VERSION,
|
|
42
|
+
TapTracker: () => TapTracker,
|
|
39
43
|
TargetRegistry: () => TargetRegistry,
|
|
40
44
|
createFlagsClient: () => import_flags6.createFlagsClient,
|
|
41
45
|
createSseParser: () => createSseParser,
|
|
@@ -63,8 +67,8 @@ __export(index_exports, {
|
|
|
63
67
|
useGuidelyTarget: () => useGuidelyTarget
|
|
64
68
|
});
|
|
65
69
|
module.exports = __toCommonJS(index_exports);
|
|
66
|
-
var
|
|
67
|
-
var
|
|
70
|
+
var import_react10 = require("react");
|
|
71
|
+
var import_react_native13 = require("react-native");
|
|
68
72
|
var import_flags3 = require("@guidelytech/flags");
|
|
69
73
|
|
|
70
74
|
// src/flags.tsx
|
|
@@ -156,7 +160,7 @@ function FlagsProvider({ children, client: external, payload, ...options }) {
|
|
|
156
160
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FlagsContext.Provider, { value, children });
|
|
157
161
|
}
|
|
158
162
|
|
|
159
|
-
// ../shared/dist/chunk-
|
|
163
|
+
// ../shared/dist/chunk-TGIUEYXM.js
|
|
160
164
|
function fnv1a(input) {
|
|
161
165
|
let h = 2166136261;
|
|
162
166
|
for (let i = 0; i < input.length; i++) {
|
|
@@ -336,6 +340,8 @@ function resolveField(rule, ctx) {
|
|
|
336
340
|
return ctx.now.getHours();
|
|
337
341
|
case "rollout.percentage":
|
|
338
342
|
return rolloutBucket(ctx.userId || ctx.anonymousId, ctx.rolloutSalt || "guidely");
|
|
343
|
+
case "experiment.bucket":
|
|
344
|
+
return rule.key ? rolloutBucket(ctx.userId || ctx.anonymousId, rule.key) : void 0;
|
|
339
345
|
case "environment":
|
|
340
346
|
return ctx.environment;
|
|
341
347
|
case "locale":
|
|
@@ -346,6 +352,9 @@ function resolveField(rule, ctx) {
|
|
|
346
352
|
return ctx.screen;
|
|
347
353
|
case "app.version":
|
|
348
354
|
return ctx.appVersion;
|
|
355
|
+
case "page.views":
|
|
356
|
+
case "feature.clicks":
|
|
357
|
+
return rule.key ? ctx.tagUsage?.[rule.key] ?? 0 : 0;
|
|
349
358
|
default:
|
|
350
359
|
return void 0;
|
|
351
360
|
}
|
|
@@ -454,8 +463,8 @@ function sanitizeProperties(props) {
|
|
|
454
463
|
for (const [k, v] of Object.entries(props)) {
|
|
455
464
|
if (SENSITIVE_PROPERTY_PATTERN.test(k)) continue;
|
|
456
465
|
if (typeof v === "function" || typeof v === "symbol") continue;
|
|
457
|
-
const
|
|
458
|
-
size += k.length + (
|
|
466
|
+
const str2 = typeof v === "string" ? v : JSON.stringify(v ?? null);
|
|
467
|
+
size += k.length + (str2?.length ?? 0);
|
|
459
468
|
if (size > MAX_PROPERTY_BYTES) break;
|
|
460
469
|
out[k.slice(0, 100)] = typeof v === "string" ? v.slice(0, 1e3) : v;
|
|
461
470
|
}
|
|
@@ -612,53 +621,252 @@ function isArticleListed(collections, article, member) {
|
|
|
612
621
|
}
|
|
613
622
|
return true;
|
|
614
623
|
}
|
|
624
|
+
var RC_MODULE_IDS = ["announcements", "search", "checklists", "guides", "articles", "links", "faqs"];
|
|
625
|
+
function orderedModules(modules) {
|
|
626
|
+
const known = (modules ?? []).filter((m, i, all) => RC_MODULE_IDS.includes(m.id) && all.findIndex((x) => x.id === m.id) === i);
|
|
627
|
+
const missing2 = RC_MODULE_IDS.filter((id) => !known.some((m) => m.id === id)).map((id) => ({ id, enabled: true }));
|
|
628
|
+
return [...known, ...missing2];
|
|
629
|
+
}
|
|
630
|
+
function visibleAnnouncements(items, member, now = Date.now()) {
|
|
631
|
+
return (items ?? []).filter((a) => {
|
|
632
|
+
const t = Date.parse(a.publishedAt);
|
|
633
|
+
return !Number.isNaN(t) && t <= now && matchesAudiences(a.audienceIds, member);
|
|
634
|
+
}).sort((a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt));
|
|
635
|
+
}
|
|
636
|
+
function unreadAnnouncements(items, lastSeenAt) {
|
|
637
|
+
const seen = lastSeenAt ? Date.parse(lastSeenAt) : NaN;
|
|
638
|
+
if (Number.isNaN(seen)) return items.length;
|
|
639
|
+
return items.filter((a) => Date.parse(a.publishedAt) > seen).length;
|
|
640
|
+
}
|
|
615
641
|
var CAPTURE_SCREENSHOT_MAX_BYTES = 8 * 1024 * 1024;
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const
|
|
620
|
-
if (
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
642
|
+
var DEFAULT_LOCALE = "en";
|
|
643
|
+
function normalizeLocale(input) {
|
|
644
|
+
if (typeof input !== "string") return "";
|
|
645
|
+
const parts = input.trim().replace(/_/g, "-").split("-").filter(Boolean);
|
|
646
|
+
if (!parts.length || !/^[a-zA-Z]{2,3}$/.test(parts[0])) return "";
|
|
647
|
+
return parts.slice(0, 3).map((p, i) => i === 0 ? p.toLowerCase() : p.length === 4 ? p[0].toUpperCase() + p.slice(1).toLowerCase() : p.length === 2 ? p.toUpperCase() : p).join("-");
|
|
648
|
+
}
|
|
649
|
+
function baseLanguage(locale) {
|
|
650
|
+
return normalizeLocale(locale).split("-")[0] ?? "";
|
|
651
|
+
}
|
|
652
|
+
function resolveLocale(requested, available, defaultLocale = DEFAULT_LOCALE) {
|
|
653
|
+
const def = normalizeLocale(defaultLocale) || DEFAULT_LOCALE;
|
|
654
|
+
const avail = available.map(normalizeLocale).filter(Boolean);
|
|
655
|
+
const all = [def, ...avail.filter((a) => a.toLowerCase() !== def.toLowerCase())];
|
|
656
|
+
for (const raw of requested) {
|
|
657
|
+
const want = normalizeLocale(raw);
|
|
658
|
+
if (!want) continue;
|
|
659
|
+
const exact = all.find((a) => a.toLowerCase() === want.toLowerCase());
|
|
660
|
+
if (exact) return exact;
|
|
661
|
+
const base = baseLanguage(want);
|
|
662
|
+
const sameBase = all.find((a) => a.toLowerCase() === base) ?? all.find((a) => baseLanguage(a) === base);
|
|
663
|
+
if (sameBase) return sameBase;
|
|
664
|
+
}
|
|
665
|
+
return def;
|
|
666
|
+
}
|
|
667
|
+
function visitorLocales(explicit, traits, platform = []) {
|
|
668
|
+
const out = [];
|
|
669
|
+
const push = (v) => {
|
|
670
|
+
const n = normalizeLocale(v);
|
|
671
|
+
if (n && !out.includes(n)) out.push(n);
|
|
672
|
+
};
|
|
673
|
+
push(explicit);
|
|
674
|
+
push(traits?.language);
|
|
675
|
+
push(traits?.locale);
|
|
676
|
+
for (const p of platform) push(p);
|
|
677
|
+
return out;
|
|
626
678
|
}
|
|
627
|
-
function
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
679
|
+
function guideLocales(config) {
|
|
680
|
+
const t = config.localization?.translations ?? {};
|
|
681
|
+
return Object.keys(t).filter((l) => Object.values(t[l] ?? {}).some((v) => typeof v === "string" && v.trim() !== ""));
|
|
682
|
+
}
|
|
683
|
+
function pick(map, key, fallback) {
|
|
684
|
+
const v = map?.[key];
|
|
685
|
+
return typeof v === "string" && v.trim() !== "" ? v : fallback;
|
|
686
|
+
}
|
|
687
|
+
function applyGuideTranslations(config, map) {
|
|
688
|
+
if (!map || !Object.keys(map).length) return config;
|
|
689
|
+
return {
|
|
690
|
+
...config,
|
|
691
|
+
steps: config.steps.map((s) => ({
|
|
692
|
+
...s,
|
|
693
|
+
title: pick(map, `steps.${s.id}.title`, s.title),
|
|
694
|
+
body: pick(map, `steps.${s.id}.body`, s.body),
|
|
695
|
+
customHtml: pick(map, `steps.${s.id}.customHtml`, s.customHtml),
|
|
696
|
+
imageAlt: pick(map, `steps.${s.id}.imageAlt`, s.imageAlt),
|
|
697
|
+
buttons: s.buttons.map((b) => ({ ...b, label: pick(map, `steps.${s.id}.buttons.${b.id}.label`, b.label) }))
|
|
698
|
+
})),
|
|
699
|
+
checklist: config.checklist ? {
|
|
700
|
+
...config.checklist,
|
|
701
|
+
launcherLabel: pick(map, "checklist.launcherLabel", config.checklist.launcherLabel),
|
|
702
|
+
completionTitle: pick(map, "checklist.completionTitle", config.checklist.completionTitle),
|
|
703
|
+
completionBody: pick(map, "checklist.completionBody", config.checklist.completionBody),
|
|
704
|
+
tasks: config.checklist.tasks.map((t) => ({ ...t, title: pick(map, `checklist.tasks.${t.id}.title`, t.title), description: pick(map, `checklist.tasks.${t.id}.description`, t.description) }))
|
|
705
|
+
} : config.checklist,
|
|
706
|
+
survey: config.survey ? {
|
|
707
|
+
...config.survey,
|
|
708
|
+
thankYouTitle: pick(map, "survey.thankYouTitle", config.survey.thankYouTitle),
|
|
709
|
+
thankYouBody: pick(map, "survey.thankYouBody", config.survey.thankYouBody),
|
|
710
|
+
questions: config.survey.questions.map((q) => ({
|
|
711
|
+
...q,
|
|
712
|
+
title: pick(map, `survey.questions.${q.id}.title`, q.title),
|
|
713
|
+
description: pick(map, `survey.questions.${q.id}.description`, q.description),
|
|
714
|
+
placeholder: pick(map, `survey.questions.${q.id}.placeholder`, q.placeholder),
|
|
715
|
+
lowLabel: pick(map, `survey.questions.${q.id}.lowLabel`, q.lowLabel),
|
|
716
|
+
highLabel: pick(map, `survey.questions.${q.id}.highLabel`, q.highLabel),
|
|
717
|
+
options: q.options.map((o) => ({ ...o, label: pick(map, `survey.questions.${q.id}.options.${o.id}.label`, o.label) }))
|
|
718
|
+
}))
|
|
719
|
+
} : config.survey
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function localizeGuide(guide, requested, defaultLocale = DEFAULT_LOCALE) {
|
|
723
|
+
const translations = guide.config.localization?.translations ?? {};
|
|
724
|
+
const locale = resolveLocale(requested, guideLocales(guide.config), defaultLocale);
|
|
725
|
+
const key = Object.keys(translations).find((l) => normalizeLocale(l).toLowerCase() === locale.toLowerCase());
|
|
726
|
+
if (!key) return { ...guide, locale };
|
|
727
|
+
return { ...guide, locale, config: applyGuideTranslations(guide.config, translations[key]) };
|
|
728
|
+
}
|
|
729
|
+
function resourceCenterLocales(rc) {
|
|
730
|
+
const set = new Set(Object.keys(rc.translations ?? {}));
|
|
731
|
+
for (const a of rc.articles) for (const l of Object.keys(a.translations ?? {})) set.add(l);
|
|
732
|
+
for (const a of rc.announcements ?? []) for (const l of Object.keys(a.translations ?? {})) set.add(l);
|
|
733
|
+
return [...set];
|
|
734
|
+
}
|
|
735
|
+
function translationFor(map, locale) {
|
|
736
|
+
if (!map) return void 0;
|
|
737
|
+
const key = Object.keys(map).find((l) => normalizeLocale(l).toLowerCase() === locale.toLowerCase());
|
|
738
|
+
return key ? map[key] : void 0;
|
|
739
|
+
}
|
|
740
|
+
function localizeArticle(a, locale) {
|
|
741
|
+
const t = translationFor(a.translations, locale);
|
|
742
|
+
if (!t) return a;
|
|
743
|
+
const body = t.body?.trim() ? t.body : a.body;
|
|
744
|
+
return { ...a, title: t.title?.trim() ? t.title : a.title, summary: t.summary?.trim() ? t.summary : a.summary, body, ...t.bodyOmitted ? { bodyOmitted: true } : {} };
|
|
745
|
+
}
|
|
746
|
+
function localizeAnnouncement(a, locale) {
|
|
747
|
+
const t = translationFor(a.translations, locale);
|
|
748
|
+
if (!t) return a;
|
|
749
|
+
return { ...a, title: t.title?.trim() ? t.title : a.title, body: t.body?.trim() ? t.body : a.body };
|
|
750
|
+
}
|
|
751
|
+
function localizeResourceCenter(rc, requested, defaultLocale = DEFAULT_LOCALE) {
|
|
752
|
+
const locale = resolveLocale(requested, resourceCenterLocales(rc), defaultLocale);
|
|
753
|
+
if (locale.toLowerCase() === normalizeLocale(defaultLocale).toLowerCase()) return { ...rc, locale };
|
|
754
|
+
const map = translationFor(rc.translations, locale);
|
|
755
|
+
return {
|
|
756
|
+
...rc,
|
|
757
|
+
locale,
|
|
758
|
+
title: pick(map, "title", rc.title),
|
|
759
|
+
launcher: { ...rc.launcher, label: pick(map, "launcher.label", rc.launcher.label) },
|
|
760
|
+
collections: rc.collections.map((c) => ({ ...c, name: pick(map, `collections.${c.id}.name`, c.name), description: pick(map, `collections.${c.id}.description`, c.description) })),
|
|
761
|
+
links: rc.links.map((l) => ({ ...l, label: pick(map, `links.${l.id}.label`, l.label) })),
|
|
762
|
+
faqs: rc.faqs.map((f) => ({ ...f, question: pick(map, `faqs.${f.id}.question`, f.question), answer: pick(map, `faqs.${f.id}.answer`, f.answer) })),
|
|
763
|
+
articles: rc.articles.map((a) => localizeArticle(a, locale)),
|
|
764
|
+
announcements: rc.announcements?.map((a) => localizeAnnouncement(a, locale))
|
|
765
|
+
};
|
|
632
766
|
}
|
|
633
|
-
var KEYS = {
|
|
634
|
-
anon: "guidely:anon",
|
|
635
|
-
seen: "guidely:seen",
|
|
636
|
-
config: (key) => `guidely:config:${key}`,
|
|
637
|
-
etag: (key) => `guidely:etag:${key}`,
|
|
638
|
-
state: (key) => `guidely:state:${key}`,
|
|
639
|
-
user: (key) => `guidely:user:${key}`,
|
|
640
|
-
queue: (key) => `guidely:queue:${key}`,
|
|
641
|
-
session: "guidely:session",
|
|
642
|
-
active: "guidely:active",
|
|
643
|
-
authoring: "guidely:authoring",
|
|
644
|
-
/** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
|
|
645
|
-
capture: "guidely:capture",
|
|
646
|
-
capturePending: "guidely:capture:pending",
|
|
647
|
-
/** Messenger conversations this browser can read: [{ id, token }]. */
|
|
648
|
-
conversations: (key) => `guidely:conversations:${key}`,
|
|
649
|
-
/** Announcement ids opened in the messenger's News tab. */
|
|
650
|
-
newsSeen: (key) => `guidely:news:${key}`
|
|
651
|
-
};
|
|
652
767
|
|
|
653
|
-
// ../sdk-core/dist/chunk-
|
|
768
|
+
// ../sdk-core/dist/chunk-JUSM7J5N.js
|
|
654
769
|
var __defProp2 = Object.defineProperty;
|
|
655
770
|
var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
656
771
|
var __publicField2 = (obj, key, value) => __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
772
|
+
var MAX_RETRY_AFTER_MS = 60 * 60 * 1e3;
|
|
773
|
+
var OFFLINE_WAIT_MS = 3e4;
|
|
774
|
+
var ServerHealth = class {
|
|
775
|
+
constructor(opts = {}) {
|
|
776
|
+
__publicField2(this, "failures", 0);
|
|
777
|
+
__publicField2(this, "openUntil", 0);
|
|
778
|
+
__publicField2(this, "lastOkAt", 0);
|
|
779
|
+
__publicField2(this, "listeners", /* @__PURE__ */ new Set());
|
|
780
|
+
__publicField2(this, "threshold");
|
|
781
|
+
__publicField2(this, "baseMs");
|
|
782
|
+
__publicField2(this, "maxMs");
|
|
783
|
+
__publicField2(this, "now");
|
|
784
|
+
__publicField2(this, "random");
|
|
785
|
+
this.threshold = opts.threshold ?? 3;
|
|
786
|
+
this.baseMs = opts.baseMs ?? 5e3;
|
|
787
|
+
this.maxMs = opts.maxMs ?? 5 * 60 * 1e3;
|
|
788
|
+
this.now = opts.now ?? Date.now;
|
|
789
|
+
this.random = opts.random ?? Math.random;
|
|
790
|
+
}
|
|
791
|
+
/** The server answered (2xx, 3xx, or a 4xx other than 429): it is up. */
|
|
792
|
+
success() {
|
|
793
|
+
const wasDown = this.failures > 0 || this.openUntil > 0;
|
|
794
|
+
this.failures = 0;
|
|
795
|
+
this.openUntil = 0;
|
|
796
|
+
this.lastOkAt = this.now();
|
|
797
|
+
if (wasDown) this.notify();
|
|
798
|
+
}
|
|
799
|
+
/** The request failed or the server is overloaded. `retryAfterMs` comes from a `Retry-After` header. */
|
|
800
|
+
failure(retryAfterMs) {
|
|
801
|
+
this.failures += 1;
|
|
802
|
+
const now = this.now();
|
|
803
|
+
if (retryAfterMs != null && retryAfterMs > 0) {
|
|
804
|
+
this.openUntil = Math.max(this.openUntil, now + Math.min(retryAfterMs, MAX_RETRY_AFTER_MS));
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
if (this.failures < this.threshold) return;
|
|
808
|
+
const cooldown = Math.min(this.maxMs, this.baseMs * 2 ** (this.failures - this.threshold));
|
|
809
|
+
this.openUntil = Math.max(this.openUntil, now + cooldown * (0.8 + this.random() * 0.4));
|
|
810
|
+
}
|
|
811
|
+
/** Milliseconds background traffic should wait before trying again (0: send now). */
|
|
812
|
+
waitMs() {
|
|
813
|
+
if (typeof navigator !== "undefined" && navigator.onLine === false) return OFFLINE_WAIT_MS;
|
|
814
|
+
return Math.max(0, this.openUntil - this.now());
|
|
815
|
+
}
|
|
816
|
+
canSend() {
|
|
817
|
+
return this.waitMs() === 0;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* The last request got an answer. Unload beacons can't report back, so what they carry is only
|
|
821
|
+
* forgotten while this holds; otherwise it stays queued and is sent again (the server drops duplicates).
|
|
822
|
+
*/
|
|
823
|
+
get reachable() {
|
|
824
|
+
return this.failures === 0 && this.lastOkAt > 0;
|
|
825
|
+
}
|
|
826
|
+
/** The browser came back online: end the cooldown and let queued traffic try straight away. */
|
|
827
|
+
online() {
|
|
828
|
+
this.openUntil = 0;
|
|
829
|
+
this.notify();
|
|
830
|
+
}
|
|
831
|
+
/** Called when the server answers again after failures, or the browser comes back online. */
|
|
832
|
+
onRecover(cb) {
|
|
833
|
+
this.listeners.add(cb);
|
|
834
|
+
return () => this.listeners.delete(cb);
|
|
835
|
+
}
|
|
836
|
+
notify() {
|
|
837
|
+
for (const cb of Array.from(this.listeners)) {
|
|
838
|
+
try {
|
|
839
|
+
cb();
|
|
840
|
+
} catch {
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
846
|
+
if (!value) return null;
|
|
847
|
+
const trimmed = value.trim();
|
|
848
|
+
if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
|
|
849
|
+
const at = Date.parse(trimmed);
|
|
850
|
+
return Number.isNaN(at) ? null : Math.max(0, at - now);
|
|
851
|
+
}
|
|
852
|
+
function isServerFailure(status) {
|
|
853
|
+
return status === 429 || status >= 500;
|
|
854
|
+
}
|
|
855
|
+
var STREAM_RESPONSE_TIMEOUT_MS = 3e4;
|
|
856
|
+
var STREAM_IDLE_TIMEOUT_MS = 6e4;
|
|
657
857
|
var Api = class {
|
|
658
|
-
constructor(baseUrl, key, fetchImpl) {
|
|
858
|
+
constructor(baseUrl, key, fetchImpl, health) {
|
|
659
859
|
__publicField2(this, "baseUrl", baseUrl);
|
|
660
860
|
__publicField2(this, "key", key);
|
|
661
861
|
__publicField2(this, "fetchImpl", fetchImpl);
|
|
862
|
+
__publicField2(this, "health");
|
|
863
|
+
this.health = health ?? new ServerHealth();
|
|
864
|
+
}
|
|
865
|
+
/** Feeds an answer (or a failed request, `null`) into {@link health}. */
|
|
866
|
+
observe(res) {
|
|
867
|
+
if (!res) this.health.failure();
|
|
868
|
+
else if (isServerFailure(res.status)) this.health.failure(parseRetryAfter(res.headers?.get?.("retry-after")));
|
|
869
|
+
else this.health.success();
|
|
662
870
|
}
|
|
663
871
|
url(path, params = {}) {
|
|
664
872
|
const query = Object.entries({ key: this.key, ...params }).filter((e) => e[1] != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
@@ -678,15 +886,22 @@ var Api = class {
|
|
|
678
886
|
const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
679
887
|
const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
|
|
680
888
|
try {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
889
|
+
let res;
|
|
890
|
+
try {
|
|
891
|
+
res = await f(this.url(path, options.params), {
|
|
892
|
+
method,
|
|
893
|
+
headers,
|
|
894
|
+
body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
|
|
895
|
+
keepalive: options.keepalive,
|
|
896
|
+
signal: ac?.signal,
|
|
897
|
+
credentials: "omit",
|
|
898
|
+
mode: "cors"
|
|
899
|
+
});
|
|
900
|
+
} catch (err) {
|
|
901
|
+
this.observe(null);
|
|
902
|
+
throw err;
|
|
903
|
+
}
|
|
904
|
+
this.observe(res);
|
|
690
905
|
let data = null;
|
|
691
906
|
if (res.status !== 304 && res.status !== 204) {
|
|
692
907
|
try {
|
|
@@ -712,30 +927,53 @@ var Api = class {
|
|
|
712
927
|
}
|
|
713
928
|
/**
|
|
714
929
|
* POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
|
|
715
|
-
* returned instead.
|
|
930
|
+
* returned instead. Scout replies can take a while, so there is no overall timeout: the response
|
|
931
|
+
* must start within 30 s and then never go quiet for more than 60 s (the server sends keepalives),
|
|
932
|
+
* otherwise the request is aborted and rejects.
|
|
716
933
|
*/
|
|
717
934
|
async stream(path, body, onEvent, headers = {}) {
|
|
718
935
|
const f = this.fetcher;
|
|
719
936
|
if (!f) throw new Error("fetch is not available");
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
let
|
|
937
|
+
const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
938
|
+
let timer = null;
|
|
939
|
+
const arm = (ms) => {
|
|
940
|
+
if (!ac) return;
|
|
941
|
+
if (timer) clearTimeout(timer);
|
|
942
|
+
timer = setTimeout(() => ac.abort(), ms);
|
|
943
|
+
};
|
|
944
|
+
arm(STREAM_RESPONSE_TIMEOUT_MS);
|
|
945
|
+
try {
|
|
946
|
+
let res;
|
|
730
947
|
try {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
948
|
+
res = await f(this.url(path), {
|
|
949
|
+
method: "POST",
|
|
950
|
+
headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
|
|
951
|
+
body: JSON.stringify(body),
|
|
952
|
+
signal: ac?.signal,
|
|
953
|
+
credentials: "omit",
|
|
954
|
+
mode: "cors"
|
|
955
|
+
});
|
|
956
|
+
} catch (err) {
|
|
957
|
+
this.observe(null);
|
|
958
|
+
throw err;
|
|
959
|
+
}
|
|
960
|
+
this.observe(res);
|
|
961
|
+
const type = res.headers?.get?.("content-type") ?? "";
|
|
962
|
+
if (!type.includes("text/event-stream") || !res.body) {
|
|
963
|
+
let data = null;
|
|
964
|
+
try {
|
|
965
|
+
data = await res.json();
|
|
966
|
+
} catch {
|
|
967
|
+
data = null;
|
|
968
|
+
}
|
|
969
|
+
return { status: res.status, ok: res.ok, data, etag: null };
|
|
734
970
|
}
|
|
735
|
-
|
|
971
|
+
arm(STREAM_IDLE_TIMEOUT_MS);
|
|
972
|
+
await readEventStream(res.body, onEvent, () => arm(STREAM_IDLE_TIMEOUT_MS));
|
|
973
|
+
return { status: res.status, ok: res.ok, data: null, etag: null };
|
|
974
|
+
} finally {
|
|
975
|
+
if (timer) clearTimeout(timer);
|
|
736
976
|
}
|
|
737
|
-
await readEventStream(res.body, onEvent);
|
|
738
|
-
return { status: res.status, ok: res.ok, data: null, etag: null };
|
|
739
977
|
}
|
|
740
978
|
/** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
|
|
741
979
|
beacon(path, body) {
|
|
@@ -766,7 +1004,7 @@ async function withRetry(send, attempts = 3, baseMs = 500) {
|
|
|
766
1004
|
function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
|
|
767
1005
|
return withRetry(() => api.post(path, body), attempts, baseMs);
|
|
768
1006
|
}
|
|
769
|
-
async function readEventStream(body, onEvent) {
|
|
1007
|
+
async function readEventStream(body, onEvent, onChunk) {
|
|
770
1008
|
const reader = body.getReader();
|
|
771
1009
|
const decoder = new TextDecoder();
|
|
772
1010
|
let buffer = "";
|
|
@@ -789,6 +1027,7 @@ async function readEventStream(body, onEvent) {
|
|
|
789
1027
|
};
|
|
790
1028
|
for (; ; ) {
|
|
791
1029
|
const { done, value } = await reader.read();
|
|
1030
|
+
if (value) onChunk?.();
|
|
792
1031
|
if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
|
|
793
1032
|
let sep;
|
|
794
1033
|
while ((sep = buffer.indexOf("\n\n")) >= 0) {
|
|
@@ -799,8 +1038,46 @@ async function readEventStream(body, onEvent) {
|
|
|
799
1038
|
}
|
|
800
1039
|
if (buffer.trim()) dispatch(buffer);
|
|
801
1040
|
}
|
|
1041
|
+
function readJson(store, key, fallback) {
|
|
1042
|
+
const raw = store.get(key);
|
|
1043
|
+
if (raw == null) return fallback;
|
|
1044
|
+
try {
|
|
1045
|
+
return JSON.parse(raw);
|
|
1046
|
+
} catch {
|
|
1047
|
+
return fallback;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
function writeJson(store, key, value) {
|
|
1051
|
+
try {
|
|
1052
|
+
store.set(key, JSON.stringify(value));
|
|
1053
|
+
} catch {
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
var KEYS = {
|
|
1057
|
+
anon: "guidely:anon",
|
|
1058
|
+
seen: "guidely:seen",
|
|
1059
|
+
config: (key) => `guidely:config:${key}`,
|
|
1060
|
+
etag: (key) => `guidely:etag:${key}`,
|
|
1061
|
+
state: (key) => `guidely:state:${key}`,
|
|
1062
|
+
user: (key) => `guidely:user:${key}`,
|
|
1063
|
+
queue: (key) => `guidely:queue:${key}`,
|
|
1064
|
+
/** Survey answers and guide/checklist progress not yet accepted by the server. */
|
|
1065
|
+
outbox: (key) => `guidely:outbox:${key}`,
|
|
1066
|
+
/** Agent Analytics turns not yet accepted by the server. */
|
|
1067
|
+
agentOutbox: (key) => `guidely:agents:${key}`,
|
|
1068
|
+
session: "guidely:session",
|
|
1069
|
+
active: "guidely:active",
|
|
1070
|
+
authoring: "guidely:authoring",
|
|
1071
|
+
/** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
|
|
1072
|
+
capture: "guidely:capture",
|
|
1073
|
+
capturePending: "guidely:capture:pending",
|
|
1074
|
+
/** Messenger conversations this browser can read: [{ id, token }]. */
|
|
1075
|
+
conversations: (key) => `guidely:conversations:${key}`,
|
|
1076
|
+
/** Announcement ids opened in the messenger's News tab. */
|
|
1077
|
+
newsSeen: (key) => `guidely:news:${key}`
|
|
1078
|
+
};
|
|
802
1079
|
|
|
803
|
-
// ../sdk-core/dist/chunk-
|
|
1080
|
+
// ../sdk-core/dist/chunk-ZAF2KRJA.js
|
|
804
1081
|
var EventQueue = class {
|
|
805
1082
|
constructor(opts) {
|
|
806
1083
|
__publicField2(this, "opts", opts);
|
|
@@ -811,17 +1088,34 @@ var EventQueue = class {
|
|
|
811
1088
|
__publicField2(this, "inflightIds", /* @__PURE__ */ new Set());
|
|
812
1089
|
__publicField2(this, "attempt", 0);
|
|
813
1090
|
__publicField2(this, "destroyed", false);
|
|
1091
|
+
__publicField2(this, "offRecover", null);
|
|
814
1092
|
__publicField2(this, "flushIntervalMs");
|
|
815
1093
|
__publicField2(this, "maxQueue");
|
|
816
1094
|
__publicField2(this, "batchSize");
|
|
817
1095
|
__publicField2(this, "maxAttempts");
|
|
818
1096
|
this.flushIntervalMs = opts.flushIntervalMs ?? 2e3;
|
|
819
|
-
this.maxQueue = opts.maxQueue ??
|
|
1097
|
+
this.maxQueue = opts.maxQueue ?? 500;
|
|
820
1098
|
this.batchSize = opts.batchSize ?? 50;
|
|
821
1099
|
this.maxAttempts = opts.maxAttempts ?? 5;
|
|
822
1100
|
const persisted = readJson(opts.store, opts.storageKey, []);
|
|
823
1101
|
if (Array.isArray(persisted)) this.events = persisted.filter((e) => e && typeof e.eventId === "string").slice(-this.maxQueue);
|
|
824
1102
|
if (this.events.length) this.schedule();
|
|
1103
|
+
this.offRecover = opts.health?.onRecover(() => this.retryNow()) ?? null;
|
|
1104
|
+
}
|
|
1105
|
+
/** The API is back: drop any pending backoff and send what is queued. */
|
|
1106
|
+
retryNow() {
|
|
1107
|
+
if (this.destroyed) return;
|
|
1108
|
+
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
1109
|
+
this.retryTimer = null;
|
|
1110
|
+
this.attempt = 0;
|
|
1111
|
+
void this.flush();
|
|
1112
|
+
}
|
|
1113
|
+
retryIn(ms) {
|
|
1114
|
+
if (this.retryTimer || this.destroyed) return;
|
|
1115
|
+
this.retryTimer = setTimeout(() => {
|
|
1116
|
+
this.retryTimer = null;
|
|
1117
|
+
void this.flush();
|
|
1118
|
+
}, ms);
|
|
825
1119
|
}
|
|
826
1120
|
get size() {
|
|
827
1121
|
return this.events.length;
|
|
@@ -855,6 +1149,8 @@ var EventQueue = class {
|
|
|
855
1149
|
clearTimeout(this.timer);
|
|
856
1150
|
this.timer = null;
|
|
857
1151
|
}
|
|
1152
|
+
const wait = this.opts.health?.waitMs() ?? 0;
|
|
1153
|
+
if (wait > 0) return this.retryIn(wait);
|
|
858
1154
|
this.inflight = true;
|
|
859
1155
|
const batch = this.events.slice(0, this.batchSize);
|
|
860
1156
|
this.inflightIds = new Set(batch.map((e) => e.eventId));
|
|
@@ -878,26 +1174,20 @@ var EventQueue = class {
|
|
|
878
1174
|
if (this.events.length) this.schedule();
|
|
879
1175
|
return;
|
|
880
1176
|
}
|
|
881
|
-
this.attempt
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
886
|
-
this.retryTimer = setTimeout(() => {
|
|
887
|
-
this.retryTimer = null;
|
|
888
|
-
void this.flush();
|
|
889
|
-
}, backoffMs(this.attempt - 1));
|
|
1177
|
+
this.attempt = Math.min(this.attempt + 1, this.maxAttempts);
|
|
1178
|
+
const delay = this.attempt >= this.maxAttempts ? 6e4 : backoffMs(this.attempt - 1);
|
|
1179
|
+
this.retryIn(Math.max(delay, this.opts.health?.waitMs() ?? 0));
|
|
890
1180
|
}
|
|
891
1181
|
/** Best-effort synchronous flush on unload using `sendBeacon`. */
|
|
892
1182
|
flushSync() {
|
|
893
1183
|
if (this.destroyed || !this.opts.enabled() || !this.opts.beacon) return;
|
|
894
1184
|
const batch = this.events.filter((e) => !this.inflightIds.has(e.eventId)).slice(0, this.batchSize);
|
|
895
1185
|
if (!batch.length) return;
|
|
896
|
-
if (this.opts.beacon(batch)) {
|
|
1186
|
+
if (this.opts.beacon(batch) && (this.opts.health?.reachable ?? true)) {
|
|
897
1187
|
const ids = new Set(batch.map((e) => e.eventId));
|
|
898
1188
|
this.events = this.events.filter((e) => !ids.has(e.eventId));
|
|
899
|
-
this.persist();
|
|
900
1189
|
}
|
|
1190
|
+
this.persist();
|
|
901
1191
|
}
|
|
902
1192
|
clear() {
|
|
903
1193
|
this.events = [];
|
|
@@ -905,6 +1195,7 @@ var EventQueue = class {
|
|
|
905
1195
|
}
|
|
906
1196
|
destroy() {
|
|
907
1197
|
this.destroyed = true;
|
|
1198
|
+
this.offRecover?.();
|
|
908
1199
|
if (this.timer) clearTimeout(this.timer);
|
|
909
1200
|
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
910
1201
|
this.timer = null;
|
|
@@ -912,6 +1203,88 @@ var EventQueue = class {
|
|
|
912
1203
|
this.persist();
|
|
913
1204
|
}
|
|
914
1205
|
};
|
|
1206
|
+
var Outbox = class {
|
|
1207
|
+
constructor(opts) {
|
|
1208
|
+
__publicField2(this, "opts", opts);
|
|
1209
|
+
__publicField2(this, "items");
|
|
1210
|
+
__publicField2(this, "sending", false);
|
|
1211
|
+
__publicField2(this, "attempt", 0);
|
|
1212
|
+
__publicField2(this, "timer", null);
|
|
1213
|
+
__publicField2(this, "destroyed", false);
|
|
1214
|
+
__publicField2(this, "offRecover");
|
|
1215
|
+
__publicField2(this, "maxItems");
|
|
1216
|
+
__publicField2(this, "maxAgeMs");
|
|
1217
|
+
this.maxItems = opts.maxItems ?? 50;
|
|
1218
|
+
this.maxAgeMs = opts.maxAgeMs ?? 7 * 24 * 60 * 60 * 1e3;
|
|
1219
|
+
const stored = readJson(opts.store, opts.storageKey, []);
|
|
1220
|
+
const cutoff = Date.now() - this.maxAgeMs;
|
|
1221
|
+
this.items = Array.isArray(stored) ? stored.filter((i) => i && typeof i.path === "string" && typeof i.key === "string" && i.createdAt > cutoff).slice(-this.maxItems) : [];
|
|
1222
|
+
this.offRecover = opts.health?.onRecover(() => {
|
|
1223
|
+
this.attempt = 0;
|
|
1224
|
+
this.retryIn(0);
|
|
1225
|
+
}) ?? null;
|
|
1226
|
+
if (this.items.length) this.retryIn(0);
|
|
1227
|
+
}
|
|
1228
|
+
get size() {
|
|
1229
|
+
return this.items.length;
|
|
1230
|
+
}
|
|
1231
|
+
/** Queues `POST path` and sends it now (or as soon as the API is reachable). */
|
|
1232
|
+
add(key, path, body) {
|
|
1233
|
+
if (this.destroyed) return;
|
|
1234
|
+
this.items = this.items.filter((i) => i.key !== key);
|
|
1235
|
+
this.items.push({ key, path, body, createdAt: Date.now() });
|
|
1236
|
+
if (this.items.length > this.maxItems) this.items.splice(0, this.items.length - this.maxItems);
|
|
1237
|
+
this.persist();
|
|
1238
|
+
void this.flush();
|
|
1239
|
+
}
|
|
1240
|
+
persist() {
|
|
1241
|
+
writeJson(this.opts.store, this.opts.storageKey, this.items);
|
|
1242
|
+
}
|
|
1243
|
+
retryIn(ms) {
|
|
1244
|
+
if (this.destroyed) return;
|
|
1245
|
+
if (this.timer) clearTimeout(this.timer);
|
|
1246
|
+
this.timer = setTimeout(() => {
|
|
1247
|
+
this.timer = null;
|
|
1248
|
+
void this.flush();
|
|
1249
|
+
}, ms);
|
|
1250
|
+
}
|
|
1251
|
+
/** Sends queued requests in order, one at a time. */
|
|
1252
|
+
async flush() {
|
|
1253
|
+
if (this.destroyed || this.sending || !this.items.length) return;
|
|
1254
|
+
const wait = this.opts.health?.waitMs() ?? 0;
|
|
1255
|
+
if (wait > 0) return this.retryIn(wait);
|
|
1256
|
+
this.sending = true;
|
|
1257
|
+
try {
|
|
1258
|
+
while (this.items.length && !this.destroyed) {
|
|
1259
|
+
const item = this.items[0];
|
|
1260
|
+
let retry = false;
|
|
1261
|
+
try {
|
|
1262
|
+
const res = await this.opts.api.post(item.path, item.body);
|
|
1263
|
+
retry = !res.ok && (res.status === 429 || res.status >= 500);
|
|
1264
|
+
} catch (err) {
|
|
1265
|
+
retry = true;
|
|
1266
|
+
this.opts.onError?.(err);
|
|
1267
|
+
}
|
|
1268
|
+
if (retry) {
|
|
1269
|
+
this.attempt += 1;
|
|
1270
|
+
this.retryIn(Math.max(backoffMs(this.attempt - 1, 1e3, 6e4), this.opts.health?.waitMs() ?? 0));
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
this.attempt = 0;
|
|
1274
|
+
this.items = this.items.filter((i) => i !== item);
|
|
1275
|
+
this.persist();
|
|
1276
|
+
}
|
|
1277
|
+
} finally {
|
|
1278
|
+
this.sending = false;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
destroy() {
|
|
1282
|
+
this.destroyed = true;
|
|
1283
|
+
this.offRecover?.();
|
|
1284
|
+
if (this.timer) clearTimeout(this.timer);
|
|
1285
|
+
this.timer = null;
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
915
1288
|
var Emitter = class {
|
|
916
1289
|
constructor(onError) {
|
|
917
1290
|
__publicField2(this, "onError", onError);
|
|
@@ -980,12 +1353,32 @@ function areMutuallyExclusive(a, b) {
|
|
|
980
1353
|
return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
|
|
981
1354
|
}
|
|
982
1355
|
function isWithinSchedule(guide, now = Date.now()) {
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1356
|
+
for (const w of [guide.config.schedule, guide.config.survey]) {
|
|
1357
|
+
if (!w) continue;
|
|
1358
|
+
if (w.startAt && now < Date.parse(w.startAt)) return false;
|
|
1359
|
+
if (w.endAt && now > Date.parse(w.endAt)) return false;
|
|
1360
|
+
}
|
|
1361
|
+
return true;
|
|
1362
|
+
}
|
|
1363
|
+
var MINUTE = 60 * 1e3;
|
|
1364
|
+
function countsTowardThrottle(guide) {
|
|
1365
|
+
return isExclusiveGuide(guide) && !guide.config.frequency.ignoreThrottling;
|
|
1366
|
+
}
|
|
1367
|
+
function isThrottleEligible(settings, launches, now = Date.now()) {
|
|
1368
|
+
if (!settings || !settings.enabled) return true;
|
|
1369
|
+
const times = launches.map((l) => Date.parse(l)).filter((t) => !Number.isNaN(t));
|
|
1370
|
+
const windowMs = Math.max(1, settings.windowHours) * HOUR;
|
|
1371
|
+
if (times.filter((t) => now - t < windowMs).length >= Math.max(1, settings.maxGuides)) return false;
|
|
1372
|
+
if (settings.minGapMinutes > 0 && times.some((t) => now - t < settings.minGapMinutes * MINUTE)) return false;
|
|
987
1373
|
return true;
|
|
988
1374
|
}
|
|
1375
|
+
function pruneLaunches(launches, settings, now = Date.now()) {
|
|
1376
|
+
const keepMs = Math.max(settings?.windowHours ?? 24, 1) * HOUR + (settings?.minGapMinutes ?? 0) * MINUTE;
|
|
1377
|
+
return launches.filter((l) => {
|
|
1378
|
+
const t = Date.parse(l);
|
|
1379
|
+
return !Number.isNaN(t) && now - t < keepMs;
|
|
1380
|
+
}).slice(-100);
|
|
1381
|
+
}
|
|
989
1382
|
function dismissedAfterMerge(local, server) {
|
|
990
1383
|
if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
|
|
991
1384
|
return local?.dismissedAt ?? server.dismissedAt ?? null;
|
|
@@ -994,6 +1387,115 @@ function dismissedAfterMerge(local, server) {
|
|
|
994
1387
|
// src/core/client.ts
|
|
995
1388
|
var import_flags2 = require("@guidelytech/flags");
|
|
996
1389
|
|
|
1390
|
+
// src/core/autocapture.ts
|
|
1391
|
+
var TAP_SLOP = 10;
|
|
1392
|
+
var TAP_MAX_MS = 1e3;
|
|
1393
|
+
var HEATMAP_MAX_POINTS = 40;
|
|
1394
|
+
function quoteValue(value) {
|
|
1395
|
+
return `"${value.replace(/["\\]/g, "\\$&")}"`;
|
|
1396
|
+
}
|
|
1397
|
+
function attrSelector(name, value) {
|
|
1398
|
+
return `[${name}=${quoteValue(value)}]`.slice(0, 300);
|
|
1399
|
+
}
|
|
1400
|
+
function cleanText(text) {
|
|
1401
|
+
const t = (text ?? "").replace(/\s+/g, " ").trim();
|
|
1402
|
+
return t ? t.slice(0, 100) : void 0;
|
|
1403
|
+
}
|
|
1404
|
+
function tapProperties(el, x, y, platform, maskAll) {
|
|
1405
|
+
const props = { selector: el.selector.slice(0, 300), tag: el.tag || "View", x: Math.round(x) || 0, y: Math.round(y) || 0, platform };
|
|
1406
|
+
const text = maskAll ? void 0 : cleanText(el.text);
|
|
1407
|
+
if (text) props.text = text;
|
|
1408
|
+
return props;
|
|
1409
|
+
}
|
|
1410
|
+
var r2 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 100) / 100;
|
|
1411
|
+
function heatmapPoint(x, y, el, rect, rage) {
|
|
1412
|
+
const p = { k: rage ? "r" : "c", x: Math.round(x) || 0, y: Math.round(y) || 0 };
|
|
1413
|
+
const s = el?.selector.slice(0, 120);
|
|
1414
|
+
if (s) p.s = s;
|
|
1415
|
+
if (s && rect && rect.width > 0 && rect.height > 0) {
|
|
1416
|
+
p.rx = r2((x - rect.x) / rect.width);
|
|
1417
|
+
p.ry = r2((y - rect.y) / rect.height);
|
|
1418
|
+
}
|
|
1419
|
+
return p;
|
|
1420
|
+
}
|
|
1421
|
+
var RageClickDetector = class {
|
|
1422
|
+
constructor(threshold = 3, windowMs = 1e3, radius = 30) {
|
|
1423
|
+
__publicField(this, "threshold", threshold);
|
|
1424
|
+
__publicField(this, "windowMs", windowMs);
|
|
1425
|
+
__publicField(this, "radius", radius);
|
|
1426
|
+
__publicField(this, "clicks", []);
|
|
1427
|
+
__publicField(this, "fired", false);
|
|
1428
|
+
}
|
|
1429
|
+
/** Taps in the current burst once it has fired (rage), otherwise 0. */
|
|
1430
|
+
get burst() {
|
|
1431
|
+
return this.fired ? this.clicks.length : 0;
|
|
1432
|
+
}
|
|
1433
|
+
/** Records a tap; returns the tap count when it completes a burst, otherwise 0. */
|
|
1434
|
+
add(x, y, t) {
|
|
1435
|
+
const first = this.clicks[0];
|
|
1436
|
+
const last = this.clicks[this.clicks.length - 1];
|
|
1437
|
+
if (!first || !last || t - last.t > this.windowMs || Math.hypot(x - first.x, y - first.y) > this.radius) {
|
|
1438
|
+
this.clicks = [];
|
|
1439
|
+
this.fired = false;
|
|
1440
|
+
}
|
|
1441
|
+
this.clicks = this.clicks.filter((c) => t - c.t <= this.windowMs);
|
|
1442
|
+
this.clicks.push({ x, y, t });
|
|
1443
|
+
if (this.fired || this.clicks.length < this.threshold) return 0;
|
|
1444
|
+
this.fired = true;
|
|
1445
|
+
return this.clicks.length;
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
var HeatmapBuffer = class {
|
|
1449
|
+
constructor() {
|
|
1450
|
+
/** Screen path the buffered points belong to. */
|
|
1451
|
+
__publicField(this, "url", "");
|
|
1452
|
+
__publicField(this, "points", []);
|
|
1453
|
+
}
|
|
1454
|
+
/** Adds a tap (`burst` > 0 marks it and the burst's previous taps as rage); true when the batch is full. */
|
|
1455
|
+
add(url, point, burst) {
|
|
1456
|
+
this.url = url;
|
|
1457
|
+
for (let i = Math.max(0, this.points.length - burst + 1); i < this.points.length; i++) this.points[i].k = "r";
|
|
1458
|
+
this.points.push(point);
|
|
1459
|
+
return this.points.length >= HEATMAP_MAX_POINTS;
|
|
1460
|
+
}
|
|
1461
|
+
/** Empties the buffer, returning what was in it. */
|
|
1462
|
+
take() {
|
|
1463
|
+
const points = this.points;
|
|
1464
|
+
this.points = [];
|
|
1465
|
+
return points.length ? { url: this.url, points } : null;
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
var TapTracker = class {
|
|
1469
|
+
constructor() {
|
|
1470
|
+
__publicField(this, "start", null);
|
|
1471
|
+
__publicField(this, "moved", 0);
|
|
1472
|
+
}
|
|
1473
|
+
/** A finger went down. A second finger while one is tracked cancels the gesture (not a tap). */
|
|
1474
|
+
begin(x, y, t, touches, id) {
|
|
1475
|
+
if (this.start || touches > 1) {
|
|
1476
|
+
this.start = null;
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
this.start = { x, y, t, id };
|
|
1480
|
+
this.moved = 0;
|
|
1481
|
+
}
|
|
1482
|
+
move(x, y, id) {
|
|
1483
|
+
const s = this.start;
|
|
1484
|
+
if (s && (id === void 0 || s.id === void 0 || id === s.id)) this.moved = Math.max(this.moved, Math.hypot(x - s.x, y - s.y));
|
|
1485
|
+
}
|
|
1486
|
+
/** The finger lifted: the tap position when it moved ≤ 10pt and lasted < 1s, else null. */
|
|
1487
|
+
end(x, y, t, id) {
|
|
1488
|
+
const s = this.start;
|
|
1489
|
+
if (!s || id !== void 0 && s.id !== void 0 && id !== s.id) return null;
|
|
1490
|
+
this.start = null;
|
|
1491
|
+
const moved = Math.max(this.moved, Math.hypot(x - s.x, y - s.y));
|
|
1492
|
+
return moved <= TAP_SLOP && t - s.t < TAP_MAX_MS ? { x, y } : null;
|
|
1493
|
+
}
|
|
1494
|
+
cancel() {
|
|
1495
|
+
this.start = null;
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1498
|
+
|
|
997
1499
|
// src/core/targets.ts
|
|
998
1500
|
var TargetTimeoutError = class extends Error {
|
|
999
1501
|
constructor(ids) {
|
|
@@ -1096,6 +1598,35 @@ var TargetRegistry = class {
|
|
|
1096
1598
|
return null;
|
|
1097
1599
|
}
|
|
1098
1600
|
}
|
|
1601
|
+
/**
|
|
1602
|
+
* The registered target on the current screen whose visible frame contains the point (window
|
|
1603
|
+
* coordinates); the smallest one when targets are nested. Frames that do not answer within
|
|
1604
|
+
* `timeoutMs` are skipped.
|
|
1605
|
+
*/
|
|
1606
|
+
async hitTest(x, y, timeoutMs = 500) {
|
|
1607
|
+
const candidates = [];
|
|
1608
|
+
this.targets.forEach((list, id) => {
|
|
1609
|
+
for (const handle of list) if (!(handle.screen && this.screen && handle.screen !== this.screen)) candidates.push({ id, handle });
|
|
1610
|
+
});
|
|
1611
|
+
if (!candidates.length) return null;
|
|
1612
|
+
const rects = await Promise.all(
|
|
1613
|
+
candidates.map(
|
|
1614
|
+
({ handle }) => new Promise((resolve) => {
|
|
1615
|
+
const timer = setTimeout(() => resolve(null), timeoutMs);
|
|
1616
|
+
void this.measureHandle(handle).then((r) => {
|
|
1617
|
+
clearTimeout(timer);
|
|
1618
|
+
resolve(r);
|
|
1619
|
+
});
|
|
1620
|
+
})
|
|
1621
|
+
)
|
|
1622
|
+
);
|
|
1623
|
+
let best = null;
|
|
1624
|
+
rects.forEach((r, i) => {
|
|
1625
|
+
if (!r || x < r.x || y < r.y || x > r.x + r.width || y > r.y + r.height) return;
|
|
1626
|
+
if (!best || r.width * r.height < best.rect.width * best.rect.height) best = { id: candidates[i].id, rect: r };
|
|
1627
|
+
});
|
|
1628
|
+
return best;
|
|
1629
|
+
}
|
|
1099
1630
|
/** First of `ids` that is mounted and visible. */
|
|
1100
1631
|
async resolve(ids) {
|
|
1101
1632
|
for (const id of ids) {
|
|
@@ -1467,12 +1998,29 @@ function createSseParser(onEvent) {
|
|
|
1467
1998
|
}
|
|
1468
1999
|
};
|
|
1469
2000
|
}
|
|
2001
|
+
var STREAM_RESPONSE_TIMEOUT_MS2 = 3e4;
|
|
2002
|
+
var STREAM_IDLE_TIMEOUT_MS2 = 6e4;
|
|
1470
2003
|
function xhrStreamPost() {
|
|
1471
2004
|
if (typeof XMLHttpRequest === "undefined") return null;
|
|
1472
2005
|
return (url, headers, body, onChunk) => new Promise((resolve, reject) => {
|
|
1473
2006
|
const xhr = new XMLHttpRequest();
|
|
1474
2007
|
let seen = 0;
|
|
1475
2008
|
let streamed = false;
|
|
2009
|
+
let timer = null;
|
|
2010
|
+
let settled = false;
|
|
2011
|
+
const settle = (fn) => {
|
|
2012
|
+
if (settled) return;
|
|
2013
|
+
settled = true;
|
|
2014
|
+
if (timer) clearTimeout(timer);
|
|
2015
|
+
fn();
|
|
2016
|
+
};
|
|
2017
|
+
const arm = (ms, what) => {
|
|
2018
|
+
if (timer) clearTimeout(timer);
|
|
2019
|
+
timer = setTimeout(() => {
|
|
2020
|
+
settle(() => reject(new Error(`Scout ${what} timed out`)));
|
|
2021
|
+
xhr.abort();
|
|
2022
|
+
}, ms);
|
|
2023
|
+
};
|
|
1476
2024
|
const pump = () => {
|
|
1477
2025
|
const type = xhr.getResponseHeader("content-type") || "";
|
|
1478
2026
|
if (!type.includes("text/event-stream")) return;
|
|
@@ -1485,8 +2033,15 @@ function xhrStreamPost() {
|
|
|
1485
2033
|
};
|
|
1486
2034
|
xhr.open("POST", url);
|
|
1487
2035
|
for (const [k, v] of Object.entries(headers)) xhr.setRequestHeader(k, v);
|
|
1488
|
-
xhr.
|
|
1489
|
-
|
|
2036
|
+
xhr.onreadystatechange = () => {
|
|
2037
|
+
if (!settled && xhr.readyState >= 2 && xhr.readyState < 4) arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
|
|
2038
|
+
};
|
|
2039
|
+
xhr.onprogress = () => {
|
|
2040
|
+
if (settled) return;
|
|
2041
|
+
arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
|
|
2042
|
+
pump();
|
|
2043
|
+
};
|
|
2044
|
+
xhr.onload = () => settle(() => {
|
|
1490
2045
|
pump();
|
|
1491
2046
|
let json = null;
|
|
1492
2047
|
if (!streamed) {
|
|
@@ -1497,27 +2052,74 @@ function xhrStreamPost() {
|
|
|
1497
2052
|
}
|
|
1498
2053
|
}
|
|
1499
2054
|
resolve({ status: xhr.status, json, streamed });
|
|
1500
|
-
};
|
|
1501
|
-
xhr.onerror = () => reject(new Error("Network request failed"));
|
|
2055
|
+
});
|
|
2056
|
+
xhr.onerror = () => settle(() => reject(new Error("Network request failed")));
|
|
2057
|
+
arm(STREAM_RESPONSE_TIMEOUT_MS2, "request");
|
|
1502
2058
|
xhr.send(JSON.stringify(body));
|
|
1503
2059
|
});
|
|
1504
2060
|
}
|
|
2061
|
+
function bodyReader(res) {
|
|
2062
|
+
const body = res.body;
|
|
2063
|
+
const Decoder = globalThis.TextDecoder;
|
|
2064
|
+
return body && typeof body.getReader === "function" && Decoder ? { reader: body.getReader(), decoder: new Decoder() } : null;
|
|
2065
|
+
}
|
|
1505
2066
|
function fetchStreamPost(fetchImpl) {
|
|
1506
2067
|
return async (url, headers, body, onChunk) => {
|
|
1507
|
-
const
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
2068
|
+
const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
2069
|
+
let timer = null;
|
|
2070
|
+
let timedOut = "";
|
|
2071
|
+
let fail = () => {
|
|
2072
|
+
};
|
|
2073
|
+
const expired = new Promise((_, reject) => fail = reject);
|
|
2074
|
+
expired.catch(() => {
|
|
2075
|
+
});
|
|
2076
|
+
const arm = (ms, what) => {
|
|
2077
|
+
if (timer) clearTimeout(timer);
|
|
2078
|
+
timer = setTimeout(() => {
|
|
2079
|
+
timedOut = what;
|
|
2080
|
+
ac?.abort();
|
|
2081
|
+
fail(new Error(`Scout ${what} timed out`));
|
|
2082
|
+
}, ms);
|
|
2083
|
+
};
|
|
2084
|
+
const race = (p) => Promise.race([p, expired]);
|
|
2085
|
+
arm(STREAM_RESPONSE_TIMEOUT_MS2, "request");
|
|
1515
2086
|
try {
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
2087
|
+
const res = await race(fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body), signal: ac?.signal }));
|
|
2088
|
+
arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
|
|
2089
|
+
const type = res.headers.get("content-type") || "";
|
|
2090
|
+
const sse = type.includes("text/event-stream");
|
|
2091
|
+
const streaming = sse ? bodyReader(res) : null;
|
|
2092
|
+
if (streaming) {
|
|
2093
|
+
const { reader, decoder } = streaming;
|
|
2094
|
+
for (; ; ) {
|
|
2095
|
+
const { done, value } = await race(reader.read());
|
|
2096
|
+
if (value) {
|
|
2097
|
+
arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
|
|
2098
|
+
onChunk(decoder.decode(value, { stream: true }));
|
|
2099
|
+
}
|
|
2100
|
+
if (done) break;
|
|
2101
|
+
}
|
|
2102
|
+
const rest = decoder.decode();
|
|
2103
|
+
if (rest) onChunk(rest);
|
|
2104
|
+
return { status: res.status, json: null, streamed: true };
|
|
2105
|
+
}
|
|
2106
|
+
const text = await race(res.text());
|
|
2107
|
+
if (sse) {
|
|
2108
|
+
onChunk(text);
|
|
2109
|
+
return { status: res.status, json: null, streamed: true };
|
|
2110
|
+
}
|
|
2111
|
+
let json = null;
|
|
2112
|
+
try {
|
|
2113
|
+
json = JSON.parse(text || "null");
|
|
2114
|
+
} catch {
|
|
2115
|
+
json = null;
|
|
2116
|
+
}
|
|
2117
|
+
return { status: res.status, json, streamed: false };
|
|
2118
|
+
} catch (err) {
|
|
2119
|
+
throw timedOut ? new Error(`Scout ${timedOut} timed out`) : err;
|
|
2120
|
+
} finally {
|
|
2121
|
+
if (timer) clearTimeout(timer);
|
|
1519
2122
|
}
|
|
1520
|
-
return { status: res.status, json, streamed: false };
|
|
1521
2123
|
};
|
|
1522
2124
|
}
|
|
1523
2125
|
var byNewest = (a, b) => Date.parse(b.lastMessageAt) - Date.parse(a.lastMessageAt);
|
|
@@ -1702,7 +2304,15 @@ var Messenger = class {
|
|
|
1702
2304
|
}
|
|
1703
2305
|
});
|
|
1704
2306
|
try {
|
|
1705
|
-
|
|
2307
|
+
let res;
|
|
2308
|
+
try {
|
|
2309
|
+
res = await this.deps.streamPost(this.url(path), headers, payload, (chunk) => parser.feed(chunk));
|
|
2310
|
+
} catch (err) {
|
|
2311
|
+
this.deps.health?.failure();
|
|
2312
|
+
throw err;
|
|
2313
|
+
}
|
|
2314
|
+
if (isServerFailure(res.status)) this.deps.health?.failure();
|
|
2315
|
+
else this.deps.health?.success();
|
|
1706
2316
|
parser.end();
|
|
1707
2317
|
if (res.status < 200 || res.status >= 300) throw new Error(`Request failed (${res.status})`);
|
|
1708
2318
|
if (!res.streamed && res.json) {
|
|
@@ -1774,7 +2384,8 @@ var Messenger = class {
|
|
|
1774
2384
|
this.stopThreadPolling();
|
|
1775
2385
|
this.set({ open: true, view: { kind: "article", articleId: id, from }, article: null });
|
|
1776
2386
|
try {
|
|
1777
|
-
const
|
|
2387
|
+
const locale = this.deps.locale?.();
|
|
2388
|
+
const res = await this.request("GET", `/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : {});
|
|
1778
2389
|
this.set({ article: res.article });
|
|
1779
2390
|
} catch (err) {
|
|
1780
2391
|
this.deps.onError(err);
|
|
@@ -1889,9 +2500,22 @@ var PersistentStore = class {
|
|
|
1889
2500
|
|
|
1890
2501
|
// src/core/client.ts
|
|
1891
2502
|
var SDK_NAME = "@guidelytech/react-native";
|
|
1892
|
-
var SDK_VERSION = "0.
|
|
2503
|
+
var SDK_VERSION = "0.11.0";
|
|
1893
2504
|
var DEFAULT_API_URL = "https://guidelytech.com";
|
|
1894
2505
|
var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
|
|
2506
|
+
var CACHED_START_MS = 1500;
|
|
2507
|
+
function settlesWithin(promise, ms) {
|
|
2508
|
+
return new Promise((resolve) => {
|
|
2509
|
+
const timer = setTimeout(() => resolve(false), ms);
|
|
2510
|
+
const done = () => {
|
|
2511
|
+
clearTimeout(timer);
|
|
2512
|
+
resolve(true);
|
|
2513
|
+
};
|
|
2514
|
+
promise.then(done, done);
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
var appVersionKey = (key) => `guidely:app:${key}`;
|
|
2518
|
+
var UNKNOWN_VIEW = { selector: "View", tag: "View" };
|
|
1895
2519
|
var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
1896
2520
|
var emptyState = () => ({ guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] });
|
|
1897
2521
|
var newSession = () => ({ id: `s_${randomId(16)}`, startedAt: nowIso(), lastActiveAt: nowIso(), shownGuides: [], events: [] });
|
|
@@ -1904,6 +2528,8 @@ var GuidelyClient = class {
|
|
|
1904
2528
|
__publicField(this, "store");
|
|
1905
2529
|
__publicField(this, "emitter");
|
|
1906
2530
|
__publicField(this, "queue", null);
|
|
2531
|
+
/** Survey answers, guide completions/dismissals and checklist progress, kept until the server has them. */
|
|
2532
|
+
__publicField(this, "outbox", null);
|
|
1907
2533
|
__publicField(this, "config", null);
|
|
1908
2534
|
__publicField(this, "configFetchedAt", 0);
|
|
1909
2535
|
__publicField(this, "etag", null);
|
|
@@ -1921,6 +2547,7 @@ var GuidelyClient = class {
|
|
|
1921
2547
|
__publicField(this, "activeSurvey", null);
|
|
1922
2548
|
__publicField(this, "queued", []);
|
|
1923
2549
|
__publicField(this, "flows", /* @__PURE__ */ new Map());
|
|
2550
|
+
/** Checklists on screen, with the guide as rendered (in the visitor's language). */
|
|
1924
2551
|
__publicField(this, "checklists", /* @__PURE__ */ new Map());
|
|
1925
2552
|
__publicField(this, "expanded", /* @__PURE__ */ new Set());
|
|
1926
2553
|
__publicField(this, "surveyView", null);
|
|
@@ -1940,6 +2567,27 @@ var GuidelyClient = class {
|
|
|
1940
2567
|
__publicField(this, "flags");
|
|
1941
2568
|
/** Experiments already reported this session (`experiment_viewed` is sent once per session). */
|
|
1942
2569
|
__publicField(this, "exposures", /* @__PURE__ */ new Set());
|
|
2570
|
+
// Product analytics (autocapture).
|
|
2571
|
+
__publicField(this, "rage", new RageClickDetector());
|
|
2572
|
+
__publicField(this, "heatmap", new HeatmapBuffer());
|
|
2573
|
+
/** Taps are processed in order (target frames are measured asynchronously). */
|
|
2574
|
+
__publicField(this, "tapChain", Promise.resolve());
|
|
2575
|
+
/** When the current screen's foreground measurement started; `screenLeft` once it has been reported. */
|
|
2576
|
+
__publicField(this, "screenStartedAt", 0);
|
|
2577
|
+
__publicField(this, "screenLeft", true);
|
|
2578
|
+
/** Last `app_open` (cold start or return to the foreground), for `app_background.foregroundMs`. */
|
|
2579
|
+
__publicField(this, "openedAt", Date.now());
|
|
2580
|
+
__publicField(this, "launchChecked", false);
|
|
2581
|
+
/** An identity was stored before this launch (an existing install adopting lifecycle events). */
|
|
2582
|
+
__publicField(this, "hadIdentity", false);
|
|
2583
|
+
/** Language set with `setLocale()` (or the `locale` option). */
|
|
2584
|
+
__publicField(this, "locale", null);
|
|
2585
|
+
/** Language each shown guide was rendered in; added to its events as `locale`. */
|
|
2586
|
+
__publicField(this, "guideLocales", /* @__PURE__ */ new Map());
|
|
2587
|
+
/** The resource center in the visitor's language, recomputed when the config or language changes. */
|
|
2588
|
+
__publicField(this, "localizedRc", null);
|
|
2589
|
+
/** "What's new" last-seen time when the resource center opened (entries after it read as new). */
|
|
2590
|
+
__publicField(this, "announcementsSeenAtOpen", null);
|
|
1943
2591
|
__publicField(this, "customListeners", /* @__PURE__ */ new Map());
|
|
1944
2592
|
__publicField(this, "eventBus", {
|
|
1945
2593
|
on: (name, cb) => {
|
|
@@ -1966,9 +2614,10 @@ var GuidelyClient = class {
|
|
|
1966
2614
|
this.emitter = new Emitter((err) => this.reportError(err));
|
|
1967
2615
|
this.api = new Api(this.options.apiUrl, this.options.projectId, options.fetch);
|
|
1968
2616
|
this.store = new PersistentStore(options.storage ?? null, (err) => this.log("storage error", err));
|
|
1969
|
-
if (!options.storage) this.log("no `storage` passed: identity
|
|
2617
|
+
if (!options.storage) this.log("no `storage` passed: identity, guide history, queued events and survey answers last only for this app session, so whatever could not be sent during a Guidely outage is lost when the app restarts. Pass AsyncStorage to persist them.");
|
|
1970
2618
|
this.cleanups.push(this.targets.onTap((id) => this.handleTap(id)));
|
|
1971
2619
|
if (options.user) this.user = { ...options.user };
|
|
2620
|
+
this.locale = options.locale ? normalizeLocale(options.locale) || null : null;
|
|
1972
2621
|
this.flags = this.createFlags(options.flags);
|
|
1973
2622
|
}
|
|
1974
2623
|
createFlags(opts) {
|
|
@@ -2017,9 +2666,10 @@ var GuidelyClient = class {
|
|
|
2017
2666
|
async bootstrap() {
|
|
2018
2667
|
const key = this.options.projectId;
|
|
2019
2668
|
try {
|
|
2020
|
-
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)]);
|
|
2669
|
+
await this.store.hydrate([KEYS.anon, KEYS.seen, KEYS.session, KEYS.config(key), KEYS.etag(key), KEYS.state(key), KEYS.user(key), KEYS.queue(key), KEYS.outbox(key), KEYS.conversations(key), appVersionKey(key)]);
|
|
2021
2670
|
if (this.destroyed) return;
|
|
2022
2671
|
this.anonymousId = this.store.get(KEYS.anon) || "";
|
|
2672
|
+
this.hadIdentity = !!this.anonymousId;
|
|
2023
2673
|
if (!this.anonymousId) {
|
|
2024
2674
|
this.anonymousId = `anon_${randomId(20)}`;
|
|
2025
2675
|
this.store.set(KEYS.anon, this.anonymousId);
|
|
@@ -2037,22 +2687,28 @@ var GuidelyClient = class {
|
|
|
2037
2687
|
store: this.store,
|
|
2038
2688
|
storageKey: KEYS.queue(key),
|
|
2039
2689
|
enabled: () => this.analyticsEnabled(),
|
|
2690
|
+
health: this.api.health,
|
|
2040
2691
|
send: async (events) => {
|
|
2041
2692
|
const res = await this.api.post("/events", { events, sentAt: nowIso(), sdk: { name: SDK_NAME, version: SDK_VERSION } });
|
|
2042
2693
|
return { ok: res.ok, retry: !(res.status >= 400 && res.status < 500 && res.status !== 429) };
|
|
2043
2694
|
},
|
|
2044
2695
|
onError: (err) => this.reportError(err)
|
|
2045
2696
|
});
|
|
2697
|
+
this.outbox = new Outbox({ api: this.api, store: this.store, storageKey: KEYS.outbox(key), health: this.api.health });
|
|
2046
2698
|
this.hydrated = true;
|
|
2047
2699
|
const cached = readJson(this.store, KEYS.config(key), null);
|
|
2048
2700
|
this.etag = this.store.get(KEYS.etag(key));
|
|
2049
2701
|
if (cached && Array.isArray(cached.guides)) this.config = forPlatform(cached, this.bridge.device.platform);
|
|
2050
2702
|
this.syncMessenger();
|
|
2051
2703
|
this.pending.splice(0).forEach((fn) => fn());
|
|
2052
|
-
|
|
2704
|
+
const before = this.config?.configVersion;
|
|
2705
|
+
const fresh = this.fetchConfig();
|
|
2706
|
+
if (!this.config) await fresh;
|
|
2707
|
+
else if (!await settlesWithin(fresh, CACHED_START_MS)) void fresh.then(() => this.applyConfigChange(before));
|
|
2053
2708
|
if (this.destroyed) return;
|
|
2709
|
+
this.checkLaunch();
|
|
2054
2710
|
this.wireTimers();
|
|
2055
|
-
if (this.user?.id) await this.identify(this.user.id, this.user.traits, this.user.account);
|
|
2711
|
+
if (this.user?.id) await settlesWithin(this.identify(this.user.id, this.user.traits, this.user.account), CACHED_START_MS);
|
|
2056
2712
|
if (this.destroyed) return;
|
|
2057
2713
|
this.isReady = true;
|
|
2058
2714
|
if (this.config) this.emitter.emit("ready", this.config);
|
|
@@ -2080,6 +2736,7 @@ var GuidelyClient = class {
|
|
|
2080
2736
|
if (res.etag) this.store.set(KEYS.etag(this.options.projectId), res.etag);
|
|
2081
2737
|
else this.store.remove(KEYS.etag(this.options.projectId));
|
|
2082
2738
|
this.syncMessenger();
|
|
2739
|
+
if (this.hydrated) this.checkLaunch();
|
|
2083
2740
|
this.emitter.emit("config", this.config);
|
|
2084
2741
|
this.log("config loaded", this.config.configVersion, `${this.config.guides.length} guides`);
|
|
2085
2742
|
return true;
|
|
@@ -2092,13 +2749,15 @@ var GuidelyClient = class {
|
|
|
2092
2749
|
}
|
|
2093
2750
|
}
|
|
2094
2751
|
wireTimers() {
|
|
2095
|
-
const refresh = setInterval(() =>
|
|
2752
|
+
const refresh = setInterval(() => {
|
|
2753
|
+
if (this.api.health.canSend()) void this.refresh();
|
|
2754
|
+
}, this.options.configRefreshMs);
|
|
2096
2755
|
this.cleanups.push(() => clearInterval(refresh));
|
|
2097
2756
|
const poll = setInterval(() => {
|
|
2098
2757
|
if (!this.isReady || !this.config || this.backgroundedAt !== null) return;
|
|
2099
2758
|
const url = this.currentUrl();
|
|
2100
2759
|
const ctx = this.getContext();
|
|
2101
|
-
for (const guide of this.
|
|
2760
|
+
for (const guide of this.candidates()) if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
|
|
2102
2761
|
this.drainQueue(url, ctx);
|
|
2103
2762
|
}, 1e3);
|
|
2104
2763
|
this.cleanups.push(() => clearInterval(poll));
|
|
@@ -2108,6 +2767,11 @@ var GuidelyClient = class {
|
|
|
2108
2767
|
if (this.destroyed) return;
|
|
2109
2768
|
const before = this.config?.configVersion;
|
|
2110
2769
|
await this.fetchConfig();
|
|
2770
|
+
this.applyConfigChange(before);
|
|
2771
|
+
}
|
|
2772
|
+
/** Re-renders after a config change (refresh, or a config that arrived after start). */
|
|
2773
|
+
applyConfigChange(before) {
|
|
2774
|
+
if (this.destroyed || !this.isReady) return;
|
|
2111
2775
|
if (this.config && this.config.configVersion !== before) {
|
|
2112
2776
|
this.notify();
|
|
2113
2777
|
this.evaluate("ready");
|
|
@@ -2117,10 +2781,13 @@ var GuidelyClient = class {
|
|
|
2117
2781
|
setAppState(state) {
|
|
2118
2782
|
if (this.destroyed || !this.hydrated) return;
|
|
2119
2783
|
if (state === "background") {
|
|
2784
|
+
const wasForeground = this.backgroundedAt === null;
|
|
2120
2785
|
this.backgroundedAt = Date.now();
|
|
2786
|
+
if (wasForeground) this.wentToBackground();
|
|
2121
2787
|
this.session.lastActiveAt = nowIso();
|
|
2122
2788
|
this.persistSession();
|
|
2123
2789
|
void this.queue?.flush();
|
|
2790
|
+
void this.outbox?.flush();
|
|
2124
2791
|
void this.store.flush();
|
|
2125
2792
|
void this.flags.flush();
|
|
2126
2793
|
this.messenger?.setForeground(false);
|
|
@@ -2133,8 +2800,9 @@ var GuidelyClient = class {
|
|
|
2133
2800
|
this.persistSession();
|
|
2134
2801
|
this.firedDelays.clear();
|
|
2135
2802
|
}
|
|
2803
|
+
this.cameToForeground();
|
|
2136
2804
|
void this.flags.refresh();
|
|
2137
|
-
if (Date.now() - this.configFetchedAt > this.options.configRefreshMs) void this.refresh();
|
|
2805
|
+
if (Date.now() - this.configFetchedAt > this.options.configRefreshMs && this.api.health.canSend()) void this.refresh();
|
|
2138
2806
|
else if (this.isReady) this.evaluate("ready");
|
|
2139
2807
|
}
|
|
2140
2808
|
}
|
|
@@ -2145,6 +2813,7 @@ var GuidelyClient = class {
|
|
|
2145
2813
|
/** Tears everything down. Create a new client to start again. */
|
|
2146
2814
|
destroy() {
|
|
2147
2815
|
if (this.destroyed) return;
|
|
2816
|
+
this.flushHeatmap();
|
|
2148
2817
|
this.destroyed = true;
|
|
2149
2818
|
this.active?.controller.destroy();
|
|
2150
2819
|
this.active = null;
|
|
@@ -2160,6 +2829,7 @@ var GuidelyClient = class {
|
|
|
2160
2829
|
this.cleanups.splice(0).forEach((fn) => fn());
|
|
2161
2830
|
void this.queue?.flush();
|
|
2162
2831
|
this.queue?.destroy();
|
|
2832
|
+
this.outbox?.destroy();
|
|
2163
2833
|
this.flags.destroy();
|
|
2164
2834
|
this.emitter.clear();
|
|
2165
2835
|
this.uiListeners.clear();
|
|
@@ -2195,6 +2865,7 @@ var GuidelyClient = class {
|
|
|
2195
2865
|
for (const [id, s] of Object.entries(data.guideState || {})) {
|
|
2196
2866
|
const local = guides[id];
|
|
2197
2867
|
guides[id] = {
|
|
2868
|
+
...local,
|
|
2198
2869
|
impressions: Math.max(local?.impressions ?? 0, s.impressions ?? 0),
|
|
2199
2870
|
completedAt: local?.completedAt ?? s.completedAt ?? null,
|
|
2200
2871
|
dismissedAt: local?.dismissedAt ?? s.dismissedAt ?? null,
|
|
@@ -2216,7 +2887,10 @@ var GuidelyClient = class {
|
|
|
2216
2887
|
checklists,
|
|
2217
2888
|
surveysSubmitted: Array.from(/* @__PURE__ */ new Set([...this.state.surveysSubmitted, ...data.surveysSubmitted || []])),
|
|
2218
2889
|
audienceIds: data.audienceIds || [],
|
|
2219
|
-
firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt
|
|
2890
|
+
firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt,
|
|
2891
|
+
// Device-local only (the server doesn't hold them).
|
|
2892
|
+
...this.state.throttle ? { throttle: this.state.throttle } : {},
|
|
2893
|
+
...this.state.announcementsSeenAt ? { announcementsSeenAt: this.state.announcementsSeenAt } : {}
|
|
2220
2894
|
};
|
|
2221
2895
|
if (data.firstSeenAt && Date.now() - Date.parse(data.firstSeenAt) > 6e4) this.firstVisit = false;
|
|
2222
2896
|
this.persistState();
|
|
@@ -2308,6 +2982,8 @@ var GuidelyClient = class {
|
|
|
2308
2982
|
...extra
|
|
2309
2983
|
};
|
|
2310
2984
|
if (extra.properties) event.properties = sanitizeProperties(extra.properties);
|
|
2985
|
+
const locale = extra.guideId ? this.guideLocales.get(extra.guideId) : void 0;
|
|
2986
|
+
if (locale) event.properties = { ...event.properties ?? {}, locale };
|
|
2311
2987
|
this.queue.enqueue(event);
|
|
2312
2988
|
this.emitter.emit("track", event);
|
|
2313
2989
|
return event;
|
|
@@ -2340,11 +3016,19 @@ var GuidelyClient = class {
|
|
|
2340
3016
|
if (!name) return;
|
|
2341
3017
|
this.whenHydrated(() => {
|
|
2342
3018
|
const changed = name !== this.screenName;
|
|
3019
|
+
if (changed) {
|
|
3020
|
+
this.leaveScreen();
|
|
3021
|
+
this.flushHeatmap();
|
|
3022
|
+
}
|
|
2343
3023
|
this.screenName = name;
|
|
2344
3024
|
this.targets.setScreen(name);
|
|
2345
3025
|
if (changed) this.syncFlagAttributes();
|
|
2346
3026
|
const url = this.currentUrl();
|
|
2347
|
-
if (changed)
|
|
3027
|
+
if (changed) {
|
|
3028
|
+
this.screenStartedAt = Date.now();
|
|
3029
|
+
this.screenLeft = this.backgroundedAt !== null;
|
|
3030
|
+
if (this.pageviewsOn()) this.enqueue("page_view", { properties: { ...properties, path: url, screen: name, platform: this.bridge.device.platform } });
|
|
3031
|
+
}
|
|
2348
3032
|
this.completeChecklistTasksBy("url_visited", url);
|
|
2349
3033
|
this.active?.controller.resume();
|
|
2350
3034
|
this.flows.forEach((f) => f.controller.resume());
|
|
@@ -2372,6 +3056,149 @@ var GuidelyClient = class {
|
|
|
2372
3056
|
if (this.isReady) this.evaluate("click", { targetId });
|
|
2373
3057
|
}
|
|
2374
3058
|
// ---------------------------------------------------------------------------
|
|
3059
|
+
// Product analytics (autocapture)
|
|
3060
|
+
// ---------------------------------------------------------------------------
|
|
3061
|
+
captureAllowed() {
|
|
3062
|
+
return !this.destroyed && this.analyticsEnabled();
|
|
3063
|
+
}
|
|
3064
|
+
/** A capability runs when the project enables it (absent on old servers → off), no option turns it off and analytics are allowed. */
|
|
3065
|
+
autocaptureOn(key) {
|
|
3066
|
+
const o = this.options.autocapture;
|
|
3067
|
+
return !!this.config?.settings?.autocapture?.[key] && o !== false && !(typeof o === "object" && o?.[key] === false) && this.captureAllowed();
|
|
3068
|
+
}
|
|
3069
|
+
/** `page_view` from `screen()` predates autocapture: only an explicit opt-out stops it. */
|
|
3070
|
+
pageviewsOn() {
|
|
3071
|
+
const o = this.options.autocapture;
|
|
3072
|
+
return this.config?.settings?.autocapture?.pageviews !== false && o !== false && !(typeof o === "object" && o?.pageviews === false);
|
|
3073
|
+
}
|
|
3074
|
+
/** True when taps are worth observing (clicks, rage taps or heatmaps are on); the provider skips resolving views otherwise. */
|
|
3075
|
+
isTapCaptureOn() {
|
|
3076
|
+
return this.hydrated && (this.autocaptureOn("clicks") || this.autocaptureOn("rageClicks") || this.autocaptureOn("heatmaps"));
|
|
3077
|
+
}
|
|
3078
|
+
/**
|
|
3079
|
+
* Records a tap observed by the provider: `click` for an interactive element, `rage_click` for
|
|
3080
|
+
* bursts and a heatmap point. A registered `<GuidelyTarget>` whose frame contains the point
|
|
3081
|
+
* (smallest wins) takes precedence over the resolved element and is reported as `#<id>`.
|
|
3082
|
+
*/
|
|
3083
|
+
recordTap(tap) {
|
|
3084
|
+
try {
|
|
3085
|
+
if (!this.hydrated || this.destroyed || !Number.isFinite(tap.x) || !Number.isFinite(tap.y)) return;
|
|
3086
|
+
const on = { clicks: this.autocaptureOn("clicks"), rage: this.autocaptureOn("rageClicks"), heat: this.autocaptureOn("heatmaps") };
|
|
3087
|
+
if (!on.clicks && !on.rage && !on.heat) return;
|
|
3088
|
+
const t = tap.t ?? Date.now();
|
|
3089
|
+
const url = this.screenName ? this.currentUrl() : "";
|
|
3090
|
+
this.tapChain = this.tapChain.then(() => this.processTap(tap, t, url, on)).catch((err) => this.reportError(err));
|
|
3091
|
+
} catch (err) {
|
|
3092
|
+
this.reportError(err);
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
async processTap(tap, t, url, on) {
|
|
3096
|
+
let el = tap.element ?? null;
|
|
3097
|
+
let rect = null;
|
|
3098
|
+
const target = this.targets.ids().length ? await this.targets.hitTest(tap.x, tap.y) : null;
|
|
3099
|
+
if (target) {
|
|
3100
|
+
el = { selector: `#${target.id}`, tag: el?.tag ?? tap.hit?.tag ?? "View", text: el?.text };
|
|
3101
|
+
rect = target.rect;
|
|
3102
|
+
}
|
|
3103
|
+
if (this.destroyed) return;
|
|
3104
|
+
const platform = this.bridge.device.platform;
|
|
3105
|
+
const maskAll = !!this.config?.settings?.replay?.maskAllText;
|
|
3106
|
+
const eventUrl = this.collectUrls() && url ? url : void 0;
|
|
3107
|
+
if (on.clicks && el) this.enqueue("click", { url: eventUrl, properties: { ...tapProperties(el, tap.x, tap.y, platform, maskAll) } });
|
|
3108
|
+
const count = this.rage.add(tap.x, tap.y, t);
|
|
3109
|
+
if (on.rage && count) this.enqueue("rage_click", { url: eventUrl, properties: { ...tapProperties(el ?? tap.hit ?? UNKNOWN_VIEW, tap.x, tap.y, platform, maskAll), count } });
|
|
3110
|
+
if (!on.heat) return;
|
|
3111
|
+
const burst = this.rage.burst;
|
|
3112
|
+
if (el && !rect && el.measure) rect = await el.measure().catch(() => null);
|
|
3113
|
+
if (this.destroyed) return;
|
|
3114
|
+
if (this.heatmap.url !== url) this.flushHeatmap();
|
|
3115
|
+
if (this.heatmap.add(url, heatmapPoint(tap.x, tap.y, el, rect, burst > 0), burst)) this.flushHeatmap();
|
|
3116
|
+
}
|
|
3117
|
+
/** Sends the buffered heatmap taps as one `heatmap` event attributed to the screen they happened on. */
|
|
3118
|
+
flushHeatmap() {
|
|
3119
|
+
const batch = this.heatmap.take();
|
|
3120
|
+
if (!batch || !this.captureAllowed()) return;
|
|
3121
|
+
const collect = this.collectUrls() && !!batch.url;
|
|
3122
|
+
const { width, height } = this.windowSize();
|
|
3123
|
+
this.enqueue("heatmap", {
|
|
3124
|
+
url: collect ? batch.url : void 0,
|
|
3125
|
+
properties: { ...collect ? { path: batch.url } : {}, viewportWidth: width, viewportHeight: height, pageHeight: height, platform: this.bridge.device.platform, points: batch.points }
|
|
3126
|
+
});
|
|
3127
|
+
}
|
|
3128
|
+
windowSize() {
|
|
3129
|
+
try {
|
|
3130
|
+
const s = this.bridge.windowSize?.();
|
|
3131
|
+
if (s && Number.isFinite(s.width) && Number.isFinite(s.height)) return { width: Math.round(s.width), height: Math.round(s.height) };
|
|
3132
|
+
} catch {
|
|
3133
|
+
}
|
|
3134
|
+
return { width: 0, height: 0 };
|
|
3135
|
+
}
|
|
3136
|
+
/** Ends the foreground measurement of the current screen with a `page_leave` (once per measurement). */
|
|
3137
|
+
leaveScreen() {
|
|
3138
|
+
if (!this.screenName || this.screenLeft) return;
|
|
3139
|
+
this.screenLeft = true;
|
|
3140
|
+
if (!this.autocaptureOn("pageLeave")) return;
|
|
3141
|
+
const url = this.currentUrl();
|
|
3142
|
+
const collect = this.collectUrls();
|
|
3143
|
+
this.enqueue("page_leave", {
|
|
3144
|
+
url: collect ? url : void 0,
|
|
3145
|
+
properties: { ...collect ? { path: url } : {}, screen: this.screenName, durationMs: Math.max(0, Date.now() - this.screenStartedAt), scrollDepth: 100, platform: this.bridge.device.platform }
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
/** `{ appVersion, build }` of this launch, without unknown keys. */
|
|
3149
|
+
appInfo() {
|
|
3150
|
+
const appVersion = this.options.appVersion ?? this.bridge.device.appVersion;
|
|
3151
|
+
const build = this.options.appBuild;
|
|
3152
|
+
return { ...appVersion ? { appVersion: String(appVersion) } : {}, ...build ? { build: String(build) } : {} };
|
|
3153
|
+
}
|
|
3154
|
+
/**
|
|
3155
|
+
* Once per launch, as soon as a config is known: `app_install` / `app_update` from the stored
|
|
3156
|
+
* version (when the app passes `appVersion` and `storage`), then `app_open` with `coldStart: true`.
|
|
3157
|
+
*/
|
|
3158
|
+
checkLaunch() {
|
|
3159
|
+
if (this.launchChecked || !this.config || this.destroyed || !this.hydrated) return;
|
|
3160
|
+
this.launchChecked = true;
|
|
3161
|
+
try {
|
|
3162
|
+
const on = this.autocaptureOn("appLifecycle");
|
|
3163
|
+
const info = this.appInfo();
|
|
3164
|
+
if (info.appVersion && this.options.storage) {
|
|
3165
|
+
const key = appVersionKey(this.options.projectId);
|
|
3166
|
+
const stored = readJson(this.store, key, null);
|
|
3167
|
+
if (!stored?.appVersion) {
|
|
3168
|
+
if (!this.hadIdentity && on) this.enqueue("app_install", { properties: { ...info } });
|
|
3169
|
+
} else if (stored.appVersion !== info.appVersion || !!stored.build && !!info.build && stored.build !== info.build) {
|
|
3170
|
+
if (on) this.enqueue("app_update", { properties: { ...info, previousVersion: stored.appVersion, ...stored.build ? { previousBuild: stored.build } : {} } });
|
|
3171
|
+
}
|
|
3172
|
+
writeJson(this.store, key, info);
|
|
3173
|
+
}
|
|
3174
|
+
if (on) this.enqueue("app_open", { properties: { ...info, coldStart: true } });
|
|
3175
|
+
} catch (err) {
|
|
3176
|
+
this.reportError(err);
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
wentToBackground() {
|
|
3180
|
+
try {
|
|
3181
|
+
this.leaveScreen();
|
|
3182
|
+
this.flushHeatmap();
|
|
3183
|
+
if (this.launchChecked && this.autocaptureOn("appLifecycle")) this.enqueue("app_background", { properties: { foregroundMs: Math.max(0, Date.now() - this.openedAt) } });
|
|
3184
|
+
} catch (err) {
|
|
3185
|
+
this.reportError(err);
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
cameToForeground() {
|
|
3189
|
+
try {
|
|
3190
|
+
this.openedAt = Date.now();
|
|
3191
|
+
if (this.launchChecked && this.autocaptureOn("appLifecycle")) this.enqueue("app_open", { properties: { ...this.appInfo(), coldStart: false } });
|
|
3192
|
+
if (this.screenName && this.screenLeft) {
|
|
3193
|
+
this.screenStartedAt = Date.now();
|
|
3194
|
+
this.screenLeft = false;
|
|
3195
|
+
if (this.pageviewsOn() && this.autocaptureOn("pageLeave")) this.enqueue("page_view", { properties: { path: this.currentUrl(), screen: this.screenName, platform: this.bridge.device.platform, resumed: true } });
|
|
3196
|
+
}
|
|
3197
|
+
} catch (err) {
|
|
3198
|
+
this.reportError(err);
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
// ---------------------------------------------------------------------------
|
|
2375
3202
|
// Targeting + triggers
|
|
2376
3203
|
// ---------------------------------------------------------------------------
|
|
2377
3204
|
/** Builds the targeting context for the current moment. */
|
|
@@ -2418,6 +3245,50 @@ var GuidelyClient = class {
|
|
|
2418
3245
|
if (guide.kind === "checklist") return !this.state.checklists[guide.id]?.dismissedAt;
|
|
2419
3246
|
return isFrequencyEligible(guide.config.frequency, this.state.guides[guide.id], this.session.shownGuides.includes(guide.id));
|
|
2420
3247
|
}
|
|
3248
|
+
/** Guide throttling: may this guide launch automatically now? */
|
|
3249
|
+
throttleAllows(guide) {
|
|
3250
|
+
if (!countsTowardThrottle(guide)) return true;
|
|
3251
|
+
return isThrottleEligible(this.config?.settings.throttling, this.state.throttle ?? []);
|
|
3252
|
+
}
|
|
3253
|
+
recordLaunch(guide, manual) {
|
|
3254
|
+
if (manual || !countsTowardThrottle(guide) || !this.config?.settings.throttling?.enabled) return;
|
|
3255
|
+
this.state.throttle = pruneLaunches([...this.state.throttle ?? [], nowIso()], this.config.settings.throttling);
|
|
3256
|
+
}
|
|
3257
|
+
/** Guides and A/B test control groups, which are evaluated the same way but never render. */
|
|
3258
|
+
candidates() {
|
|
3259
|
+
const guides = this.config?.guides ?? [];
|
|
3260
|
+
const holdouts = this.config?.guideHoldouts ?? [];
|
|
3261
|
+
return holdouts.length ? [...guides, ...holdouts.map((h) => h.guide)] : guides;
|
|
3262
|
+
}
|
|
3263
|
+
holdoutFor(guide) {
|
|
3264
|
+
return this.config?.guideHoldouts?.find((h) => h.guide.id === guide.id) ?? null;
|
|
3265
|
+
}
|
|
3266
|
+
// ---------------------------------------------------------------------------
|
|
3267
|
+
// Localization
|
|
3268
|
+
// ---------------------------------------------------------------------------
|
|
3269
|
+
/** The visitor's language candidates, most preferred first: `setLocale()` / `locale` option, traits, device. */
|
|
3270
|
+
locales() {
|
|
3271
|
+
return visitorLocales(this.locale, this.user?.traits, [this.bridge.device.locale]);
|
|
3272
|
+
}
|
|
3273
|
+
defaultLocale() {
|
|
3274
|
+
return this.config?.settings?.localization?.defaultLocale || "en";
|
|
3275
|
+
}
|
|
3276
|
+
/** The guide in the visitor's language (remembered so its events carry `locale`). */
|
|
3277
|
+
localize(guide) {
|
|
3278
|
+
const localized = localizeGuide(guide, this.locales(), this.defaultLocale());
|
|
3279
|
+
this.guideLocales.set(guide.id, localized.locale);
|
|
3280
|
+
return localized;
|
|
3281
|
+
}
|
|
3282
|
+
/**
|
|
3283
|
+
* Sets the language for guides and the resource center (`null` = back to the user's traits and
|
|
3284
|
+
* the device language). Guides already on screen keep their language; the resource center re-renders.
|
|
3285
|
+
*/
|
|
3286
|
+
setLocale(locale) {
|
|
3287
|
+
const next = locale ? normalizeLocale(locale) || null : null;
|
|
3288
|
+
if (next === this.locale) return;
|
|
3289
|
+
this.locale = next;
|
|
3290
|
+
this.notify();
|
|
3291
|
+
}
|
|
2421
3292
|
triggerFires(guide, reason, detail) {
|
|
2422
3293
|
const t = guide.config.trigger;
|
|
2423
3294
|
switch (t.type) {
|
|
@@ -2453,7 +3324,7 @@ var GuidelyClient = class {
|
|
|
2453
3324
|
this.lastAudiences = current;
|
|
2454
3325
|
}
|
|
2455
3326
|
const candidates = [];
|
|
2456
|
-
for (const guide of this.
|
|
3327
|
+
for (const guide of this.candidates()) {
|
|
2457
3328
|
const t = guide.config.trigger;
|
|
2458
3329
|
if (detail.endedGuide && detail.endedGuide.id === guide.id) continue;
|
|
2459
3330
|
if (reason === "ready" || reason === "navigation") {
|
|
@@ -2469,6 +3340,10 @@ var GuidelyClient = class {
|
|
|
2469
3340
|
if (t.type === "page_load" && t.delayMs > 0 && reason === "timer" && detail.guideId !== guide.id) continue;
|
|
2470
3341
|
if (!this.triggerFires(guide, reason, detail)) continue;
|
|
2471
3342
|
if (!this.isEligible(guide, ctx, url)) continue;
|
|
3343
|
+
if (!this.throttleAllows(guide)) {
|
|
3344
|
+
this.log("throttled", guide.key);
|
|
3345
|
+
continue;
|
|
3346
|
+
}
|
|
2472
3347
|
candidates.push(guide);
|
|
2473
3348
|
}
|
|
2474
3349
|
if (reason === "after_guide" || reason === "navigation") this.drainQueue(url, ctx);
|
|
@@ -2514,6 +3389,12 @@ var GuidelyClient = class {
|
|
|
2514
3389
|
}
|
|
2515
3390
|
tryShow(guide, url, manual) {
|
|
2516
3391
|
if (this.destroyed) return false;
|
|
3392
|
+
if (!manual && !this.throttleAllows(guide)) return false;
|
|
3393
|
+
if (this.holdoutFor(guide)) {
|
|
3394
|
+
if (this.hasExclusive() && !manual) return false;
|
|
3395
|
+
this.recordImpression(guide, manual);
|
|
3396
|
+
return true;
|
|
3397
|
+
}
|
|
2517
3398
|
if (!isExclusiveGuide(guide)) {
|
|
2518
3399
|
if (this.flows.has(guide.id) || this.checklists.has(guide.id)) return false;
|
|
2519
3400
|
this.show(guide, manual);
|
|
@@ -2555,14 +3436,18 @@ var GuidelyClient = class {
|
|
|
2555
3436
|
guideState(id) {
|
|
2556
3437
|
return this.state.guides[id] ?? (this.state.guides[id] = { impressions: 0 });
|
|
2557
3438
|
}
|
|
2558
|
-
recordImpression(guide) {
|
|
3439
|
+
recordImpression(guide, manual = false) {
|
|
2559
3440
|
const s = this.guideState(guide.id);
|
|
2560
3441
|
s.impressions += 1;
|
|
2561
3442
|
s.lastShownAt = nowIso();
|
|
2562
3443
|
if (!this.session.shownGuides.includes(guide.id)) this.session.shownGuides.push(guide.id);
|
|
3444
|
+
this.recordLaunch(guide, manual);
|
|
2563
3445
|
this.persistState();
|
|
2564
3446
|
this.persistSession();
|
|
2565
|
-
this.
|
|
3447
|
+
const holdout = this.holdoutFor(guide);
|
|
3448
|
+
const experiment = holdout ? { id: holdout.experimentId, variantId: holdout.variantId } : guide.experiment;
|
|
3449
|
+
if (experiment) this.enqueue("experiment_viewed", { properties: { guideExperimentId: experiment.id, variantId: experiment.variantId, ...holdout ? {} : { guideId: guide.id } } });
|
|
3450
|
+
if (!holdout) this.syncGuideState(guide.id, { impression: true });
|
|
2566
3451
|
}
|
|
2567
3452
|
recordEnd(guide, reason) {
|
|
2568
3453
|
const s = this.guideState(guide.id);
|
|
@@ -2573,7 +3458,13 @@ var GuidelyClient = class {
|
|
|
2573
3458
|
if (reason === "completed") this.completeChecklistTasksBy("guide_completed", guide);
|
|
2574
3459
|
}
|
|
2575
3460
|
syncGuideState(guideId, update) {
|
|
2576
|
-
|
|
3461
|
+
const body = { anonymousId: this.anonymousId, userId: this.user?.id || null, updates: [{ guideId, ...update }] };
|
|
3462
|
+
if (update.impression) void postWithRetry(this.api, "/guide-state", body);
|
|
3463
|
+
else this.sendDurably(`guide:${guideId}:${update.completed ? "completed" : "dismissed"}:${this.anonymousId}`, "/guide-state", body);
|
|
3464
|
+
}
|
|
3465
|
+
/** Queues a write in the persisted outbox (sent now, or once the API is reachable again). */
|
|
3466
|
+
sendDurably(key, path, body) {
|
|
3467
|
+
this.whenHydrated(() => this.outbox?.add(key, path, body));
|
|
2577
3468
|
}
|
|
2578
3469
|
show(guide, manual, stepId) {
|
|
2579
3470
|
try {
|
|
@@ -2584,7 +3475,8 @@ var GuidelyClient = class {
|
|
|
2584
3475
|
this.reportError(err);
|
|
2585
3476
|
}
|
|
2586
3477
|
}
|
|
2587
|
-
showFlow(
|
|
3478
|
+
showFlow(raw, manual, stepId) {
|
|
3479
|
+
const guide = this.localize(raw);
|
|
2588
3480
|
const exclusive = isExclusiveGuide(guide);
|
|
2589
3481
|
const flow = { guide, manual, view: null, controller: null };
|
|
2590
3482
|
let renders = 0;
|
|
@@ -2599,7 +3491,7 @@ var GuidelyClient = class {
|
|
|
2599
3491
|
const step = e.stepId ? guide.config.steps.find((s) => s.id === e.stepId) : void 0;
|
|
2600
3492
|
this.enqueue(e.name, { guideId: guide.id, guideVersion: guide.version, stepId: e.stepId, stepIndex: e.stepIndex, properties: e.properties });
|
|
2601
3493
|
if (e.name === "guide_started") {
|
|
2602
|
-
this.recordImpression(guide);
|
|
3494
|
+
this.recordImpression(guide, manual);
|
|
2603
3495
|
this.emitter.emit("guide:start", guide);
|
|
2604
3496
|
} else if (e.name === "step_viewed" && step) {
|
|
2605
3497
|
this.emitter.emit("guide:step", { guide, stepId: step.id, stepIndex: e.stepIndex ?? 0 });
|
|
@@ -2646,21 +3538,24 @@ var GuidelyClient = class {
|
|
|
2646
3538
|
if (this.flows.get(guide.id) === flow) this.flows.delete(guide.id);
|
|
2647
3539
|
}
|
|
2648
3540
|
}
|
|
2649
|
-
showChecklist(
|
|
3541
|
+
showChecklist(raw, open) {
|
|
3542
|
+
if (!raw.config.checklist || this.checklists.has(raw.id)) return;
|
|
3543
|
+
const guide = this.localize(raw);
|
|
2650
3544
|
const config = guide.config.checklist;
|
|
2651
|
-
if (!config
|
|
3545
|
+
if (!config) return;
|
|
2652
3546
|
const progress = this.getChecklistProgress(guide.id);
|
|
2653
3547
|
const first = !this.state.guides[guide.id]?.impressions;
|
|
2654
|
-
this.checklists.set(guide.id, { open: open ?? (config.autoOpen && first && !progress.completedAt) });
|
|
3548
|
+
this.checklists.set(guide.id, { guide, open: open ?? (config.autoOpen && first && !progress.completedAt) });
|
|
2655
3549
|
if (first) this.recordImpression(guide);
|
|
2656
3550
|
if (this.checklists.get(guide.id)?.open) this.enqueue("guide_viewed", { guideId: guide.id, guideVersion: guide.version });
|
|
2657
3551
|
this.notify();
|
|
2658
3552
|
}
|
|
2659
|
-
showSurvey(
|
|
2660
|
-
if (!
|
|
3553
|
+
showSurvey(raw, manual) {
|
|
3554
|
+
if (!raw.config.survey) return;
|
|
3555
|
+
const guide = this.localize(raw);
|
|
2661
3556
|
this.activeSurvey = { guide, manual };
|
|
2662
3557
|
this.surveyView = { key: `${guide.id}:${randomId(6)}`, guide, submitted: false };
|
|
2663
|
-
this.recordImpression(guide);
|
|
3558
|
+
this.recordImpression(guide, manual);
|
|
2664
3559
|
this.enqueue("survey_displayed", { guideId: guide.id, guideVersion: guide.version });
|
|
2665
3560
|
this.emitter.emit("guide:start", guide);
|
|
2666
3561
|
this.notify();
|
|
@@ -2720,10 +3615,7 @@ var GuidelyClient = class {
|
|
|
2720
3615
|
const steps = [];
|
|
2721
3616
|
for (const f of [...this.flows.values(), ...this.active ? [this.active] : []]) if (f.view) steps.push({ ...f.view, expanded: this.expanded.has(f.guide.id) });
|
|
2722
3617
|
const checklists = [];
|
|
2723
|
-
this.checklists.forEach((w, id) => {
|
|
2724
|
-
const guide = this.findGuide(id);
|
|
2725
|
-
if (guide) checklists.push({ guide, progress: this.getChecklistProgress(id), open: w.open });
|
|
2726
|
-
});
|
|
3618
|
+
this.checklists.forEach((w, id) => checklists.push({ guide: w.guide, progress: this.getChecklistProgress(id), open: w.open }));
|
|
2727
3619
|
this.ui = { steps, checklists, survey: this.surveyView, resourceCenterOpen: this.resourceCenterOpen, theme: this.themeFor(), messenger: !!this.messenger };
|
|
2728
3620
|
for (const cb of Array.from(this.uiListeners)) {
|
|
2729
3621
|
try {
|
|
@@ -2781,7 +3673,7 @@ var GuidelyClient = class {
|
|
|
2781
3673
|
progress.dismissedAt = nowIso();
|
|
2782
3674
|
this.persistState();
|
|
2783
3675
|
this.enqueue("checklist_dismissed", { guideId: guide.id, guideVersion: guide.version });
|
|
2784
|
-
|
|
3676
|
+
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 });
|
|
2785
3677
|
this.checklists.delete(guide.id);
|
|
2786
3678
|
this.notify();
|
|
2787
3679
|
}
|
|
@@ -2794,7 +3686,7 @@ var GuidelyClient = class {
|
|
|
2794
3686
|
this.persistState();
|
|
2795
3687
|
this.recordEnd(guide, "completed");
|
|
2796
3688
|
this.enqueue("survey_submitted", { guideId: guide.id, guideVersion: guide.version, properties: { responseId } });
|
|
2797
|
-
|
|
3689
|
+
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 });
|
|
2798
3690
|
this.emitter.emit("survey:submit", { guide, answers, responseId });
|
|
2799
3691
|
this.emitter.emit("guide:complete", guide);
|
|
2800
3692
|
this.surveyView = { ...this.surveyView, submitted: true };
|
|
@@ -2808,10 +3700,58 @@ var GuidelyClient = class {
|
|
|
2808
3700
|
getAudienceIds() {
|
|
2809
3701
|
return this.state.audienceIds;
|
|
2810
3702
|
}
|
|
3703
|
+
/** The published resource center in the visitor's language, or null when it is off. */
|
|
2811
3704
|
getResourceCenter() {
|
|
2812
|
-
const rc = this.
|
|
3705
|
+
const rc = this.localizedResourceCenter();
|
|
2813
3706
|
return rc && rc.enabled ? rc : null;
|
|
2814
3707
|
}
|
|
3708
|
+
/** The resource center in the visitor's language (cached until the config or language changes). */
|
|
3709
|
+
localizedResourceCenter() {
|
|
3710
|
+
const rc = this.config?.resourceCenter;
|
|
3711
|
+
if (!rc) return null;
|
|
3712
|
+
const locales = this.locales();
|
|
3713
|
+
const key = `${locales.join(",")}|${this.defaultLocale()}`;
|
|
3714
|
+
if (this.localizedRc?.source !== rc || this.localizedRc.locales !== key) this.localizedRc = { source: rc, locales: key, value: localizeResourceCenter(rc, locales, this.defaultLocale()) };
|
|
3715
|
+
return this.localizedRc.value;
|
|
3716
|
+
}
|
|
3717
|
+
/** Evaluates targeting rules (e.g. a resource-center section's) for the current visitor and screen. */
|
|
3718
|
+
matchesTargeting(rules) {
|
|
3719
|
+
if (!rules) return true;
|
|
3720
|
+
try {
|
|
3721
|
+
return evaluateRules(rules, this.getContext());
|
|
3722
|
+
} catch (err) {
|
|
3723
|
+
this.reportError(err);
|
|
3724
|
+
return false;
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
/** Resource-center sections this visitor sees, in order (disabled and non-matching ones left out). */
|
|
3728
|
+
getResourceCenterModules() {
|
|
3729
|
+
const rc = this.getResourceCenter();
|
|
3730
|
+
if (!rc) return [];
|
|
3731
|
+
return orderedModules(rc.modules).filter((m) => m.enabled && (!("targeting" in m) || this.matchesTargeting(m.targeting))).map((m) => m.id);
|
|
3732
|
+
}
|
|
3733
|
+
/**
|
|
3734
|
+
* Sections of the messenger's Help tab, in order: the resource center's modules without "What's
|
|
3735
|
+
* new" (the messenger has its own News tab); every section when no resource center is published.
|
|
3736
|
+
*/
|
|
3737
|
+
getHelpModules() {
|
|
3738
|
+
const modules = this.getResourceCenter() ? this.getResourceCenterModules() : [...RC_MODULE_IDS];
|
|
3739
|
+
return modules.filter((id) => id !== "announcements");
|
|
3740
|
+
}
|
|
3741
|
+
/** "What's new" entries this visitor sees (newest first); empty when the section is off. */
|
|
3742
|
+
getAnnouncements() {
|
|
3743
|
+
const rc = this.getResourceCenter();
|
|
3744
|
+
if (!rc || !this.getResourceCenterModules().includes("announcements")) return [];
|
|
3745
|
+
return visibleAnnouncements(rc.announcements, this.state.audienceIds);
|
|
3746
|
+
}
|
|
3747
|
+
/** "What's new" entries published since the visitor last opened the resource center. */
|
|
3748
|
+
getUnreadAnnouncements() {
|
|
3749
|
+
return unreadAnnouncements(this.getAnnouncements(), this.state.announcementsSeenAt);
|
|
3750
|
+
}
|
|
3751
|
+
/** Whether an entry is new to the visitor in the resource center that is open now. */
|
|
3752
|
+
isAnnouncementNew(a) {
|
|
3753
|
+
return unreadAnnouncements([a], this.announcementsSeenAtOpen) > 0;
|
|
3754
|
+
}
|
|
2815
3755
|
async searchResourceCenter(q) {
|
|
2816
3756
|
try {
|
|
2817
3757
|
const res = await this.api.get("/resource-center/search", { params: { q } });
|
|
@@ -2820,6 +3760,11 @@ var GuidelyClient = class {
|
|
|
2820
3760
|
return [];
|
|
2821
3761
|
}
|
|
2822
3762
|
}
|
|
3763
|
+
/** The language of the resource center as shown, when it isn't the default (sent with article fetches). */
|
|
3764
|
+
articleLocale() {
|
|
3765
|
+
const locale = this.localizedResourceCenter()?.locale;
|
|
3766
|
+
return locale && locale.toLowerCase() !== this.defaultLocale().toLowerCase() ? locale : void 0;
|
|
3767
|
+
}
|
|
2823
3768
|
articleViewed(article) {
|
|
2824
3769
|
this.enqueue("article_viewed", { properties: { articleId: article.id, slug: article.slug } });
|
|
2825
3770
|
}
|
|
@@ -2851,7 +3796,7 @@ var GuidelyClient = class {
|
|
|
2851
3796
|
this.enqueue("checklist_completed", { guideId: guide.id, guideVersion: guide.version });
|
|
2852
3797
|
}
|
|
2853
3798
|
this.persistState();
|
|
2854
|
-
|
|
3799
|
+
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 });
|
|
2855
3800
|
this.emitter.emit("checklist:task", { guide, task, progress: this.getChecklistProgress(guide.id) });
|
|
2856
3801
|
if (complete) {
|
|
2857
3802
|
this.emitter.emit("checklist:complete", guide);
|
|
@@ -2954,6 +3899,11 @@ var GuidelyClient = class {
|
|
|
2954
3899
|
}
|
|
2955
3900
|
if (this.resourceCenterOpen) return;
|
|
2956
3901
|
this.resourceCenterOpen = true;
|
|
3902
|
+
this.announcementsSeenAtOpen = this.state.announcementsSeenAt ?? null;
|
|
3903
|
+
if (this.getAnnouncements().length) {
|
|
3904
|
+
this.state.announcementsSeenAt = nowIso();
|
|
3905
|
+
this.persistState();
|
|
3906
|
+
}
|
|
2957
3907
|
this.enqueue("resource_center_opened");
|
|
2958
3908
|
this.emitter.emit("resource-center:open");
|
|
2959
3909
|
this.notify();
|
|
@@ -2988,7 +3938,9 @@ var GuidelyClient = class {
|
|
|
2988
3938
|
},
|
|
2989
3939
|
onMessage: (message, conversation) => this.emitter.emit("conversation:message", { message, conversation }),
|
|
2990
3940
|
onStartGuide: (key) => this.setTimer(() => this.startGuide(key), 0),
|
|
2991
|
-
onError: (err) => this.log("messenger", err instanceof Error ? err.message : err)
|
|
3941
|
+
onError: (err) => this.log("messenger", err instanceof Error ? err.message : err),
|
|
3942
|
+
health: this.api.health,
|
|
3943
|
+
locale: () => this.articleLocale()
|
|
2992
3944
|
});
|
|
2993
3945
|
this.resourceCenterOpen = false;
|
|
2994
3946
|
}
|
|
@@ -3093,6 +4045,7 @@ var GuidelyClient = class {
|
|
|
3093
4045
|
audienceIds: this.state.audienceIds,
|
|
3094
4046
|
session: this.session,
|
|
3095
4047
|
pendingEvents: this.queue?.size ?? 0,
|
|
4048
|
+
pendingWrites: this.outbox?.size ?? 0,
|
|
3096
4049
|
targets: this.targets.ids(),
|
|
3097
4050
|
flagsReady: this.flags.isReady()
|
|
3098
4051
|
};
|
|
@@ -3121,7 +4074,191 @@ var GuidelyClient = class {
|
|
|
3121
4074
|
}
|
|
3122
4075
|
};
|
|
3123
4076
|
function forPlatform(config, platform) {
|
|
3124
|
-
|
|
4077
|
+
const keep = (g) => supportsPlatform(g.config, platform);
|
|
4078
|
+
const holdouts = config.guideHoldouts?.filter((h) => keep(h.guide)).map((h) => ({ ...h, guide: withoutCode(h.guide) }));
|
|
4079
|
+
return { ...config, guides: config.guides.filter(keep).map(withoutCode), ...holdouts ? { guideHoldouts: holdouts } : {} };
|
|
4080
|
+
}
|
|
4081
|
+
function withoutCode(guide) {
|
|
4082
|
+
if (!guide.config.steps.some((s) => s.code)) return guide;
|
|
4083
|
+
return { ...guide, config: { ...guide.config, steps: guide.config.steps.map(({ code: _code, ...step }) => step) } };
|
|
4084
|
+
}
|
|
4085
|
+
|
|
4086
|
+
// src/ui/Autocapture.tsx
|
|
4087
|
+
var import_react8 = require("react");
|
|
4088
|
+
var import_react_native11 = require("react-native");
|
|
4089
|
+
|
|
4090
|
+
// src/core/viewTree.ts
|
|
4091
|
+
var HOST_TEXT = 6;
|
|
4092
|
+
var MAX_ANCESTORS = 500;
|
|
4093
|
+
var MAX_TEXT_NODES = 300;
|
|
4094
|
+
var INTERACTIVE_ROLES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem", "checkbox", "switch", "radio", "togglebutton", "imagebutton", "option", "combobox", "adjustable"]);
|
|
4095
|
+
var TEXT_INPUT_HOST = /TextInput/i;
|
|
4096
|
+
var NOISE = /^(Anonymous|Unknown|ForwardRef|Memo|Context|Provider|Consumer|Fragment|StrictMode|Suspense|Profiler)$|Context$|Provider$|Consumer$/;
|
|
4097
|
+
var isFiber = (v) => !!v && typeof v === "object" && "return" in v && "memoizedProps" in v;
|
|
4098
|
+
function fiberFromEvent(event) {
|
|
4099
|
+
try {
|
|
4100
|
+
const e = event;
|
|
4101
|
+
if (isFiber(e?._targetInst)) return e._targetInst;
|
|
4102
|
+
const target = e?.target;
|
|
4103
|
+
if (!target || typeof target !== "object") return null;
|
|
4104
|
+
for (const key of ["__internalInstanceHandle", "_internalInstanceHandle", "_internalFiberInstanceHandleDEV"]) if (isFiber(target[key])) return target[key];
|
|
4105
|
+
for (const sym of Object.getOwnPropertySymbols(target)) if (sym.description === "internalInstanceHandle" && isFiber(target[sym])) return target[sym];
|
|
4106
|
+
} catch {
|
|
4107
|
+
}
|
|
4108
|
+
return null;
|
|
4109
|
+
}
|
|
4110
|
+
function componentName(type, depth = 0) {
|
|
4111
|
+
if (!type || depth > 3) return null;
|
|
4112
|
+
if (typeof type === "function") {
|
|
4113
|
+
const f = type;
|
|
4114
|
+
const n = typeof f.displayName === "string" && f.displayName ? f.displayName : typeof f.name === "string" ? f.name : "";
|
|
4115
|
+
return n || null;
|
|
4116
|
+
}
|
|
4117
|
+
if (typeof type === "object") {
|
|
4118
|
+
const o = type;
|
|
4119
|
+
if (typeof o.displayName === "string" && o.displayName) return o.displayName;
|
|
4120
|
+
return componentName(o.render, depth + 1) ?? componentName(o.type, depth + 1);
|
|
4121
|
+
}
|
|
4122
|
+
return null;
|
|
4123
|
+
}
|
|
4124
|
+
var isHost = (f) => typeof f.type === "string";
|
|
4125
|
+
var isTextInput = (f) => typeof f.type === "string" ? TEXT_INPUT_HOST.test(f.type) : componentName(f.type) === "TextInput";
|
|
4126
|
+
function isInteractive(props) {
|
|
4127
|
+
if (!props || typeof props !== "object") return null;
|
|
4128
|
+
if (typeof props.onPress === "function" || typeof props.onLongPress === "function") return "press";
|
|
4129
|
+
const role = props.accessibilityRole ?? props.role;
|
|
4130
|
+
return typeof role === "string" && INTERACTIVE_ROLES.has(role) ? "role" : null;
|
|
4131
|
+
}
|
|
4132
|
+
function firstHost(f) {
|
|
4133
|
+
let cur = f;
|
|
4134
|
+
for (let i = 0; cur && i < 30; i++, cur = cur.child) if (isHost(cur)) return cur;
|
|
4135
|
+
return null;
|
|
4136
|
+
}
|
|
4137
|
+
function idSelector(f) {
|
|
4138
|
+
const host = firstHost(f);
|
|
4139
|
+
const sources = [f.memoizedProps, host?.memoizedProps];
|
|
4140
|
+
for (const key of ["nativeID", "testID"]) {
|
|
4141
|
+
for (const p of sources) {
|
|
4142
|
+
const v = p && typeof p === "object" ? p[key] : void 0;
|
|
4143
|
+
if (typeof v === "string" && v) return attrSelector(key, v);
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
return null;
|
|
4147
|
+
}
|
|
4148
|
+
function pathSelector(f) {
|
|
4149
|
+
const parts = [];
|
|
4150
|
+
let cur = f;
|
|
4151
|
+
for (let i = 0; cur && parts.length < 4 && i < MAX_ANCESTORS; i++, cur = cur.return) {
|
|
4152
|
+
if (isHost(cur)) continue;
|
|
4153
|
+
const name = componentName(cur.type);
|
|
4154
|
+
if (!name || NOISE.test(name)) continue;
|
|
4155
|
+
let same = 0;
|
|
4156
|
+
let index = 0;
|
|
4157
|
+
for (let s = cur.return?.child, n = 0; s && n < 500; s = s.sibling, n++) {
|
|
4158
|
+
if (s.type !== cur.type) continue;
|
|
4159
|
+
same += 1;
|
|
4160
|
+
if (s === cur) index = same;
|
|
4161
|
+
}
|
|
4162
|
+
parts.unshift(same > 1 && index ? `${name}:nth-of-type(${index})` : name);
|
|
4163
|
+
}
|
|
4164
|
+
return parts.join(" > ").slice(0, 300) || "View";
|
|
4165
|
+
}
|
|
4166
|
+
function subtreeText(f, opts) {
|
|
4167
|
+
const out = [];
|
|
4168
|
+
let visited = 0;
|
|
4169
|
+
let masked = false;
|
|
4170
|
+
const walk = (node) => {
|
|
4171
|
+
for (let n = node; n && visited < MAX_TEXT_NODES && !masked; n = n.sibling) {
|
|
4172
|
+
visited += 1;
|
|
4173
|
+
if (opts.isMask?.(n.type)) {
|
|
4174
|
+
masked = true;
|
|
4175
|
+
return;
|
|
4176
|
+
}
|
|
4177
|
+
if (isTextInput(n)) continue;
|
|
4178
|
+
if (n.tag === HOST_TEXT && (typeof n.memoizedProps === "string" || typeof n.memoizedProps === "number")) out.push(String(n.memoizedProps));
|
|
4179
|
+
else walk(n.child);
|
|
4180
|
+
}
|
|
4181
|
+
};
|
|
4182
|
+
if (opts.isMask?.(f.type)) return { text: "", masked: true };
|
|
4183
|
+
walk(f.child);
|
|
4184
|
+
return { text: out.join(" "), masked };
|
|
4185
|
+
}
|
|
4186
|
+
function measureHost(host) {
|
|
4187
|
+
return new Promise((resolve) => {
|
|
4188
|
+
let settled = false;
|
|
4189
|
+
const done = (r) => {
|
|
4190
|
+
if (settled) return;
|
|
4191
|
+
settled = true;
|
|
4192
|
+
clearTimeout(timer);
|
|
4193
|
+
resolve(r);
|
|
4194
|
+
};
|
|
4195
|
+
const timer = setTimeout(() => done(null), 500);
|
|
4196
|
+
const cb = (x, y, width, height) => done(Number.isFinite(x) && Number.isFinite(y) && width > 0 && height > 0 ? { x, y, width, height } : null);
|
|
4197
|
+
try {
|
|
4198
|
+
const sn = host.stateNode;
|
|
4199
|
+
const pub = sn?.canonical?.publicInstance ?? sn;
|
|
4200
|
+
if (pub && typeof pub.measureInWindow === "function") return pub.measureInWindow(cb);
|
|
4201
|
+
const ui = globalThis.nativeFabricUIManager;
|
|
4202
|
+
if (sn?.node && typeof ui?.measureInWindow === "function") return ui.measureInWindow(sn.node, cb);
|
|
4203
|
+
} catch {
|
|
4204
|
+
}
|
|
4205
|
+
done(null);
|
|
4206
|
+
});
|
|
4207
|
+
}
|
|
4208
|
+
function describe(f, masked, opts, withText) {
|
|
4209
|
+
const props = f.memoizedProps && typeof f.memoizedProps === "object" ? f.memoizedProps : {};
|
|
4210
|
+
const host = firstHost(f);
|
|
4211
|
+
const el = { selector: idSelector(f) ?? pathSelector(f), tag: componentName(f.type) ?? "View" };
|
|
4212
|
+
if (withText && !masked) {
|
|
4213
|
+
const sub = subtreeText(f, opts);
|
|
4214
|
+
if (!sub.masked) {
|
|
4215
|
+
const hostProps = host?.memoizedProps && typeof host.memoizedProps === "object" ? host.memoizedProps : {};
|
|
4216
|
+
const label = props.accessibilityLabel ?? props["aria-label"] ?? hostProps.accessibilityLabel ?? hostProps["aria-label"];
|
|
4217
|
+
const text = cleanText(sub.text) ?? (typeof label === "string" ? cleanText(label) : void 0);
|
|
4218
|
+
if (text) el.text = text;
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
if (host) el.measure = () => measureHost(host);
|
|
4222
|
+
return el;
|
|
4223
|
+
}
|
|
4224
|
+
function resolveFiber(target, opts = {}) {
|
|
4225
|
+
const none = { ignored: false, element: null, hit: null };
|
|
4226
|
+
if (!target) return none;
|
|
4227
|
+
try {
|
|
4228
|
+
let press = null;
|
|
4229
|
+
let role = null;
|
|
4230
|
+
let hitFiber = null;
|
|
4231
|
+
let stopped = false;
|
|
4232
|
+
let masked = false;
|
|
4233
|
+
for (let f = target, i = 0; f && i < MAX_ANCESTORS; f = f.return, i++) {
|
|
4234
|
+
if (opts.isIgnored?.(f.type)) return { ignored: true, element: null, hit: null };
|
|
4235
|
+
if (opts.isMask?.(f.type)) masked = true;
|
|
4236
|
+
if (stopped) continue;
|
|
4237
|
+
if (isTextInput(f)) {
|
|
4238
|
+
stopped = true;
|
|
4239
|
+
continue;
|
|
4240
|
+
}
|
|
4241
|
+
if (isHost(f)) continue;
|
|
4242
|
+
const name = componentName(f.type);
|
|
4243
|
+
if (!hitFiber && name && !NOISE.test(name)) hitFiber = f;
|
|
4244
|
+
const kind = isInteractive(f.memoizedProps);
|
|
4245
|
+
if (kind === "press") {
|
|
4246
|
+
press = f;
|
|
4247
|
+
stopped = true;
|
|
4248
|
+
} else if (kind === "role" && !role) role = f;
|
|
4249
|
+
}
|
|
4250
|
+
const element = press ?? role;
|
|
4251
|
+
return {
|
|
4252
|
+
ignored: false,
|
|
4253
|
+
element: element ? describe(element, masked, opts, true) : null,
|
|
4254
|
+
hit: hitFiber ? describe(hitFiber, masked, opts, componentName(hitFiber.type) === "Text") : null
|
|
4255
|
+
};
|
|
4256
|
+
} catch {
|
|
4257
|
+
return none;
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
function resolveTapTarget(event, opts = {}) {
|
|
4261
|
+
return resolveFiber(fiberFromEvent(event), opts);
|
|
3125
4262
|
}
|
|
3126
4263
|
|
|
3127
4264
|
// src/ui/Overlay.tsx
|
|
@@ -3881,25 +5018,41 @@ function Help({ store, client, palette: p }) {
|
|
|
3881
5018
|
] }, title);
|
|
3882
5019
|
const audiences = client.getAudienceIds();
|
|
3883
5020
|
const sections = resourceCenterSections(articles?.collections ?? [], articles?.articles ?? [], audiences);
|
|
3884
|
-
const
|
|
5021
|
+
const modules = client.getHelpModules();
|
|
5022
|
+
const searchOn = modules.includes("search");
|
|
5023
|
+
const listed = searchOn ? results?.filter((a) => isArticleListed(articles?.collections ?? [], a, audiences)) : void 0;
|
|
5024
|
+
const checklists = (rc?.checklistGuideIds ?? []).map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g && g.kind === "checklist");
|
|
5025
|
+
const renderModule = (id) => {
|
|
5026
|
+
switch (id) {
|
|
5027
|
+
case "guides":
|
|
5028
|
+
return recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u25B6"))) : null;
|
|
5029
|
+
case "checklists":
|
|
5030
|
+
return checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u2611"))) : null;
|
|
5031
|
+
case "articles":
|
|
5032
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
|
|
5033
|
+
!articles ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.ActivityIndicator, { color: p.muted }) : null,
|
|
5034
|
+
sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => open(a)))))
|
|
5035
|
+
] });
|
|
5036
|
+
case "faqs":
|
|
5037
|
+
return rc?.faqs.length ? section(
|
|
5038
|
+
"FAQ",
|
|
5039
|
+
rc.faqs.map((f) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.View, { style: [styles4.row, styles4.faq, { borderColor: p.border }], children: [
|
|
5040
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.Pressable, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles4.faqHead, children: [
|
|
5041
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.rowTitle, styles4.flex, { color: p.text }], children: f.question }),
|
|
5042
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
|
|
5043
|
+
] }),
|
|
5044
|
+
faq === f.id ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
|
|
5045
|
+
] }, f.id))
|
|
5046
|
+
) : null;
|
|
5047
|
+
case "links":
|
|
5048
|
+
return rc?.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null;
|
|
5049
|
+
default:
|
|
5050
|
+
return null;
|
|
5051
|
+
}
|
|
5052
|
+
};
|
|
3885
5053
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.ScrollView, { contentContainerStyle: styles4.content, keyboardShouldPersistTaps: "handled", children: [
|
|
3886
|
-
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.TextInput, { value: q, onChangeText: setQ, placeholder: "Search articles", placeholderTextColor: p.muted, accessibilityLabel: "Search help articles", returnKeyType: "search", style: [styles4.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }] }),
|
|
3887
|
-
listed ? section(`Results for \u201C${q.trim()}\u201D`, listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => open(a))) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.rowSub, { color: p.muted }], children: "No articles found." })) : /* @__PURE__ */ (0, import_jsx_runtime5.
|
|
3888
|
-
recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => store.startGuide(g.key), "\u25B6"))) : null,
|
|
3889
|
-
!articles ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.ActivityIndicator, { color: p.muted }) : null,
|
|
3890
|
-
sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => open(a))))),
|
|
3891
|
-
rc?.faqs.length ? section(
|
|
3892
|
-
"FAQ",
|
|
3893
|
-
rc.faqs.map((f) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.View, { style: [styles4.row, styles4.faq, { borderColor: p.border }], children: [
|
|
3894
|
-
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.Pressable, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles4.faqHead, children: [
|
|
3895
|
-
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.rowTitle, styles4.flex, { color: p.text }], children: f.question }),
|
|
3896
|
-
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
|
|
3897
|
-
] }),
|
|
3898
|
-
faq === f.id ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
|
|
3899
|
-
] }, f.id))
|
|
3900
|
-
) : null,
|
|
3901
|
-
rc?.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null
|
|
3902
|
-
] })
|
|
5054
|
+
searchOn ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.TextInput, { value: q, onChangeText: setQ, placeholder: "Search articles", placeholderTextColor: p.muted, accessibilityLabel: "Search help articles", returnKeyType: "search", style: [styles4.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }] }) : null,
|
|
5055
|
+
listed ? section(`Results for \u201C${q.trim()}\u201D`, listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => open(a))) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.rowSub, { color: p.muted }], children: "No articles found." })) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: modules.filter((id) => id !== "search").map((id) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react3.Fragment, { children: renderModule(id) }, id)) })
|
|
3903
5056
|
] });
|
|
3904
5057
|
}
|
|
3905
5058
|
function Article({ article, palette: p, client }) {
|
|
@@ -4002,6 +5155,8 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
|
|
|
4002
5155
|
const checklists = (0, import_react4.useMemo)(() => (rc?.checklistGuideIds ?? []).map((id) => guides.find((g) => g.id === id || g.key === id)).filter((g) => !!g && g.kind === "checklist"), [rc, guides]);
|
|
4003
5156
|
if (!rc) return null;
|
|
4004
5157
|
const close = () => client.closeResourceCenter();
|
|
5158
|
+
const modules = client.getResourceCenterModules();
|
|
5159
|
+
const announcements = client.getAnnouncements();
|
|
4005
5160
|
const openArticle = (a) => {
|
|
4006
5161
|
if (a.externalUrl) return client.navigate(a.externalUrl);
|
|
4007
5162
|
client.articleViewed(a);
|
|
@@ -4020,7 +5175,51 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
|
|
|
4020
5175
|
] }, title);
|
|
4021
5176
|
const audiences = client.getAudienceIds();
|
|
4022
5177
|
const sections = resourceCenterSections(rc.collections, rc.articles, audiences);
|
|
4023
|
-
const listed = results?.filter((a) => isArticleListed(rc.collections, a, audiences));
|
|
5178
|
+
const listed = modules.includes("search") ? results?.filter((a) => isArticleListed(rc.collections, a, audiences)) : void 0;
|
|
5179
|
+
const link = (label, onPress) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress, hitSlop: 8, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.link, { color: p.primary }], children: label }) }, label);
|
|
5180
|
+
const announcement2 = (a) => {
|
|
5181
|
+
const linked = a.articleId ? rc.articles.find((x) => x.id === a.articleId) : void 0;
|
|
5182
|
+
const isNew = client.isAnnouncementNew(a);
|
|
5183
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
|
|
5184
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: styles5.faqHead, children: [
|
|
5185
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { accessibilityRole: "header", style: [styles5.rowTitle, styles5.flex, { color: p.text, fontFamily: p.fontFamily }], children: a.title }),
|
|
5186
|
+
isNew ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.badge, { color: p.onPrimary, backgroundColor: p.primary }], children: "NEW" }) : null
|
|
5187
|
+
] }),
|
|
5188
|
+
a.body ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RichText, { html: a.body, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null,
|
|
5189
|
+
a.guideId || linked || a.url ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: styles5.links, children: [
|
|
5190
|
+
a.guideId ? link("Show me", () => (close(), client.startGuide(a.guideId))) : null,
|
|
5191
|
+
linked ? link("Read more", () => openArticle(linked)) : null,
|
|
5192
|
+
a.url ? link(a.linkLabel || "Learn more", () => client.navigate(a.url)) : null
|
|
5193
|
+
] }) : null
|
|
5194
|
+
] }, a.id);
|
|
5195
|
+
};
|
|
5196
|
+
const renderModule = (id) => {
|
|
5197
|
+
switch (id) {
|
|
5198
|
+
case "announcements":
|
|
5199
|
+
return announcements.length ? section("What's new", announcements.map(announcement2)) : null;
|
|
5200
|
+
case "guides":
|
|
5201
|
+
return recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => (close(), client.startGuide(g.id)), "\u25B6"))) : null;
|
|
5202
|
+
case "checklists":
|
|
5203
|
+
return checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => (close(), client.openChecklist(g.id)), "\u2611"))) : null;
|
|
5204
|
+
case "articles":
|
|
5205
|
+
return sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => openArticle(a)))));
|
|
5206
|
+
case "faqs":
|
|
5207
|
+
return rc.faqs.length ? section(
|
|
5208
|
+
"FAQ",
|
|
5209
|
+
rc.faqs.map((f) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
|
|
5210
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.Pressable, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles5.faqHead, children: [
|
|
5211
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.rowTitle, styles5.flex, { color: p.text }], children: f.question }),
|
|
5212
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
|
|
5213
|
+
] }),
|
|
5214
|
+
faq === f.id ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
|
|
5215
|
+
] }, f.id))
|
|
5216
|
+
) : null;
|
|
5217
|
+
case "links":
|
|
5218
|
+
return rc.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null;
|
|
5219
|
+
default:
|
|
5220
|
+
return null;
|
|
5221
|
+
}
|
|
5222
|
+
};
|
|
4024
5223
|
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Modal, { visible: open, animationType: "slide", presentationStyle: import_react_native6.Platform.OS === "ios" ? "pageSheet" : "fullScreen", onRequestClose: () => article ? setArticle(null) : close(), children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.sheet, { backgroundColor: p.background, paddingTop: import_react_native6.Platform.OS === "ios" ? 8 : topInset }], children: [
|
|
4025
5224
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.header, { borderColor: p.border }], children: [
|
|
4026
5225
|
article ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Pressable, { accessibilityRole: "button", accessibilityLabel: "Back", onPress: () => setArticle(null), hitSlop: 10, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.back, { color: p.primary }], children: "\u2039 Back" }) }) : null,
|
|
@@ -4031,7 +5230,7 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
|
|
|
4031
5230
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { accessibilityRole: "header", style: [styles5.articleTitle, { color: p.text, fontFamily: p.fontFamily }], children: article.title }),
|
|
4032
5231
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RichText, { html: article.body, palette: p, onLink: (u) => client.navigate(u) })
|
|
4033
5232
|
] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.ScrollView, { contentContainerStyle: [styles5.content, { paddingBottom: bottomInset + 24 }], keyboardShouldPersistTaps: "handled", children: [
|
|
4034
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
5233
|
+
modules.includes("search") ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
4035
5234
|
import_react_native6.TextInput,
|
|
4036
5235
|
{
|
|
4037
5236
|
value: query,
|
|
@@ -4043,26 +5242,11 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
|
|
|
4043
5242
|
returnKeyType: "search",
|
|
4044
5243
|
style: [styles5.search, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(10, p.radius) }]
|
|
4045
5244
|
}
|
|
4046
|
-
),
|
|
5245
|
+
) : null,
|
|
4047
5246
|
listed ? section(
|
|
4048
5247
|
`Results for \u201C${query.trim()}\u201D`,
|
|
4049
5248
|
listed.length ? listed.map((a) => row(a.id, a.title, a.summary, () => openArticle(a))) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.rowSub, { color: p.muted }], children: "No articles found." })
|
|
4050
|
-
) : /* @__PURE__ */ (0, import_jsx_runtime6.
|
|
4051
|
-
recommended.length ? section("Recommended", recommended.map((g) => row(g.id, g.name, void 0, () => (close(), client.startGuide(g.id)), "\u25B6"))) : null,
|
|
4052
|
-
checklists.length ? section("Checklists", checklists.map((g) => row(g.id, g.name, void 0, () => (close(), client.openChecklist(g.id)), "\u2611"))) : null,
|
|
4053
|
-
sections.map((s) => section(s.collection ? s.path.join(" \u203A ") : "Articles", s.articles.map((a) => row(a.id, a.title, a.summary, () => openArticle(a))))),
|
|
4054
|
-
rc.faqs.length ? section(
|
|
4055
|
-
"FAQ",
|
|
4056
|
-
rc.faqs.map((f) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.View, { style: [styles5.row, styles5.faq, { borderColor: p.border }], children: [
|
|
4057
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native6.Pressable, { accessibilityRole: "button", accessibilityState: { expanded: faq === f.id }, onPress: () => setFaq(faq === f.id ? null : f.id), style: styles5.faqHead, children: [
|
|
4058
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.rowTitle, styles5.flex, { color: p.text }], children: f.question }),
|
|
4059
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native6.Text, { style: [styles5.chev, { color: p.muted }], children: faq === f.id ? "\u2212" : "+" })
|
|
4060
|
-
] }),
|
|
4061
|
-
faq === f.id ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RichText, { html: f.answer, palette: p, onLink: (u) => client.navigate(u), size: 14 }) : null
|
|
4062
|
-
] }, f.id))
|
|
4063
|
-
) : null,
|
|
4064
|
-
rc.links.length ? section("Links", rc.links.map((l) => row(l.id, l.label, void 0, () => client.navigate(l.url), "\u2197"))) : null
|
|
4065
|
-
] })
|
|
5249
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: modules.filter((id) => id !== "search").map((id) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react4.Fragment, { children: renderModule(id) }, id)) })
|
|
4066
5250
|
] })
|
|
4067
5251
|
] }) });
|
|
4068
5252
|
}
|
|
@@ -4082,7 +5266,10 @@ var styles5 = import_react_native6.StyleSheet.create({
|
|
|
4082
5266
|
chev: { fontSize: 18 },
|
|
4083
5267
|
faq: { flexDirection: "column", alignItems: "stretch" },
|
|
4084
5268
|
faqHead: { flexDirection: "row", alignItems: "center", gap: 10 },
|
|
4085
|
-
articleTitle: { fontSize: 22, fontWeight: "700" }
|
|
5269
|
+
articleTitle: { fontSize: 22, fontWeight: "700" },
|
|
5270
|
+
badge: { fontSize: 10, fontWeight: "800", letterSpacing: 0.5, paddingHorizontal: 6, paddingVertical: 2, borderRadius: 6, overflow: "hidden" },
|
|
5271
|
+
links: { flexDirection: "row", flexWrap: "wrap", gap: 16, marginTop: 4 },
|
|
5272
|
+
link: { fontSize: 14, fontWeight: "600" }
|
|
4086
5273
|
});
|
|
4087
5274
|
|
|
4088
5275
|
// src/ui/StepLayer.tsx
|
|
@@ -4128,7 +5315,10 @@ function positionCard(target, size, viewport, insets, placement, offset) {
|
|
|
4128
5315
|
|
|
4129
5316
|
// src/ui/StepCard.tsx
|
|
4130
5317
|
var import_react_native7 = require("react-native");
|
|
4131
|
-
var import_jsx_runtime7 =
|
|
5318
|
+
var import_jsx_runtime7 = (
|
|
5319
|
+
// An empty description marks the image as decorative: screen readers skip it.
|
|
5320
|
+
require("react/jsx-runtime")
|
|
5321
|
+
);
|
|
4132
5322
|
function StepCard({ view, client, palette: p, width, style, compact }) {
|
|
4133
5323
|
const { step, guide } = view;
|
|
4134
5324
|
const buttons = visibleButtons(step, view.canGoBack);
|
|
@@ -4136,7 +5326,16 @@ function StepCard({ view, client, palette: p, width, style, compact }) {
|
|
|
4136
5326
|
const showProgress = step.showProgress && view.total > 1;
|
|
4137
5327
|
const onLink = (url) => client.navigate(url);
|
|
4138
5328
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Surface, { palette: p, style: [{ width }, style], children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { accessible: false, accessibilityViewIsModal: view.asModal || step.type === "modal", style: compact ? styles6.compact : styles6.inner, children: [
|
|
4139
|
-
step.imageUrl ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
5329
|
+
step.imageUrl ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
5330
|
+
import_react_native7.Image,
|
|
5331
|
+
{
|
|
5332
|
+
source: { uri: step.imageUrl },
|
|
5333
|
+
style: [styles6.image, { borderRadius: Math.max(0, p.radius - 4) }],
|
|
5334
|
+
resizeMode: "cover",
|
|
5335
|
+
accessibilityIgnoresInvertColors: true,
|
|
5336
|
+
...step.imageAlt?.trim() ? { accessible: true, accessibilityRole: "image", accessibilityLabel: step.imageAlt.trim() } : { accessible: false, importantForAccessibility: "no" }
|
|
5337
|
+
}
|
|
5338
|
+
) : null,
|
|
4140
5339
|
step.title ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: styles6.header, children: [
|
|
4141
5340
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { accessibilityRole: "header", style: [styles6.title, { color: p.text, fontFamily: p.fontFamily }], children: step.title }),
|
|
4142
5341
|
step.showClose ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CloseButton, { palette: p, onPress: () => client.closeStep(guide.id), label: "Dismiss guide" }) : null
|
|
@@ -4724,18 +5923,22 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
|
|
|
4724
5923
|
const basePalette = palette(ui.theme, scheme);
|
|
4725
5924
|
const messenger = ui.messenger ? client.getMessenger() : null;
|
|
4726
5925
|
const showRcLauncher = !messenger && !!client.options.showResourceCenterLauncher && !!client.getResourceCenter();
|
|
5926
|
+
const unread = showRcLauncher ? client.getUnreadAnnouncements() : 0;
|
|
4727
5927
|
const messengerLeft = messenger?.getSnapshot().config.launcher.position === "bottom-left";
|
|
4728
5928
|
const checklistOnSameSide = ui.checklists.some((c) => c.guide.config.checklist?.position === "bottom-left" === messengerLeft) && client.options.showChecklistLauncher !== false;
|
|
4729
5929
|
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native10.View, { ref, collapsable: false, pointerEvents: "box-none", style: import_react_native10.StyleSheet.absoluteFill, onLayout: measureOrigin, children: [
|
|
4730
5930
|
ui.checklists.map((c) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Checklist, { view: c, client, palette: palette(client.themeFor(c.guide), scheme), geometry, showLauncher: client.options.showChecklistLauncher !== false, launcherBottom: launcherOffset }, c.guide.id)),
|
|
4731
|
-
showRcLauncher ? /* @__PURE__ */ (0, import_jsx_runtime10.
|
|
5931
|
+
showRcLauncher ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
|
|
4732
5932
|
import_react_native10.Pressable,
|
|
4733
5933
|
{
|
|
4734
5934
|
accessibilityRole: "button",
|
|
4735
|
-
accessibilityLabel: client
|
|
5935
|
+
accessibilityLabel: rcLauncherLabel(client),
|
|
4736
5936
|
onPress: () => client.openResourceCenter(),
|
|
4737
5937
|
style: [styles9.rc, client.getResourceCenter()?.launcher.position === "bottom-left" ? { left: insets.left + 16 } : { right: insets.right + 16 }, { bottom: insets.bottom + launcherOffset + (ui.checklists.length ? 56 : 0), backgroundColor: basePalette.primary }],
|
|
4738
|
-
children:
|
|
5938
|
+
children: [
|
|
5939
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native10.Text, { style: [styles9.rcText, { color: basePalette.onPrimary }], children: "?" }),
|
|
5940
|
+
unread ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native10.View, { pointerEvents: "none", style: [styles9.rcBadge, { borderColor: basePalette.primary }], children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native10.Text, { style: styles9.rcBadgeText, children: unread > 9 ? "9+" : unread }) }) : null
|
|
5941
|
+
]
|
|
4739
5942
|
}
|
|
4740
5943
|
) : null,
|
|
4741
5944
|
messenger && client.options.showMessengerLauncher !== false ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MessengerLauncher, { store: messenger, palette: basePalette, style: [messengerLeft ? { left: insets.left + 16 } : { right: insets.right + 16 }, { bottom: insets.bottom + launcherOffset + (checklistOnSameSide ? 56 : 0) }] }) : null,
|
|
@@ -4746,28 +5949,76 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
|
|
|
4746
5949
|
}
|
|
4747
5950
|
var styles9 = import_react_native10.StyleSheet.create({
|
|
4748
5951
|
rc: { position: "absolute", width: 48, height: 48, borderRadius: 24, alignItems: "center", justifyContent: "center", elevation: 8, shadowColor: "#000", shadowOpacity: 0.2, shadowRadius: 10, shadowOffset: { width: 0, height: 4 } },
|
|
4749
|
-
rcText: { fontSize: 22, fontWeight: "800" }
|
|
5952
|
+
rcText: { fontSize: 22, fontWeight: "800" },
|
|
5953
|
+
rcBadge: { position: "absolute", top: -4, right: -4, minWidth: 20, height: 20, paddingHorizontal: 5, borderRadius: 10, borderWidth: 2, backgroundColor: "#dc2626", alignItems: "center", justifyContent: "center" },
|
|
5954
|
+
rcBadgeText: { color: "#ffffff", fontSize: 11, fontWeight: "800" }
|
|
4750
5955
|
});
|
|
5956
|
+
function rcLauncherLabel(client) {
|
|
5957
|
+
const label = client.getResourceCenter()?.launcher.label || "Help";
|
|
5958
|
+
const unread = client.getUnreadAnnouncements();
|
|
5959
|
+
return unread ? `${label} (${unread} new update${unread === 1 ? "" : "s"})` : label;
|
|
5960
|
+
}
|
|
4751
5961
|
|
|
4752
|
-
// src/ui/
|
|
4753
|
-
var import_react8 = require("react");
|
|
4754
|
-
var import_react_native11 = require("react-native");
|
|
5962
|
+
// src/ui/Autocapture.tsx
|
|
4755
5963
|
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
4756
|
-
|
|
4757
|
-
|
|
5964
|
+
function GuidelyMask({ children, ...rest }) {
|
|
5965
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native11.View, { ...rest, children });
|
|
5966
|
+
}
|
|
5967
|
+
var RESOLVE = {
|
|
5968
|
+
isIgnored: (type) => type === GuidelyOverlay,
|
|
5969
|
+
isMask: (type) => type === GuidelyMask
|
|
5970
|
+
};
|
|
5971
|
+
function useTapObserver(client) {
|
|
5972
|
+
const clientRef = (0, import_react8.useRef)(client);
|
|
5973
|
+
clientRef.current = client;
|
|
5974
|
+
return (0, import_react8.useMemo)(() => {
|
|
5975
|
+
const tracker = new TapTracker();
|
|
5976
|
+
const guard = (fn) => (e) => {
|
|
5977
|
+
try {
|
|
5978
|
+
fn(e);
|
|
5979
|
+
} catch {
|
|
5980
|
+
tracker.cancel();
|
|
5981
|
+
}
|
|
5982
|
+
};
|
|
5983
|
+
return {
|
|
5984
|
+
onTouchStartCapture: guard((e) => {
|
|
5985
|
+
const n = e.nativeEvent;
|
|
5986
|
+
if (!clientRef.current?.isTapCaptureOn()) return tracker.cancel();
|
|
5987
|
+
tracker.begin(n.pageX, n.pageY, Date.now(), Array.isArray(n.touches) ? n.touches.length : 1, n.identifier);
|
|
5988
|
+
}),
|
|
5989
|
+
onTouchMoveCapture: guard((e) => tracker.move(e.nativeEvent.pageX, e.nativeEvent.pageY, e.nativeEvent.identifier)),
|
|
5990
|
+
onTouchEndCapture: guard((e) => {
|
|
5991
|
+
const n = e.nativeEvent;
|
|
5992
|
+
const tap = tracker.end(n.pageX, n.pageY, Date.now(), n.identifier);
|
|
5993
|
+
const c = clientRef.current;
|
|
5994
|
+
if (!tap || !c) return;
|
|
5995
|
+
const r = resolveTapTarget(e, RESOLVE);
|
|
5996
|
+
if (!r.ignored) c.recordTap({ x: tap.x, y: tap.y, t: Date.now(), element: r.element, hit: r.hit });
|
|
5997
|
+
}),
|
|
5998
|
+
onTouchCancelCapture: guard(() => tracker.cancel())
|
|
5999
|
+
};
|
|
6000
|
+
}, []);
|
|
6001
|
+
}
|
|
6002
|
+
|
|
6003
|
+
// src/ui/Target.tsx
|
|
6004
|
+
var import_react9 = require("react");
|
|
6005
|
+
var import_react_native12 = require("react-native");
|
|
6006
|
+
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
6007
|
+
var GuidelyContext = (0, import_react9.createContext)(null);
|
|
6008
|
+
var GuidelyScreenContext = (0, import_react9.createContext)(null);
|
|
4758
6009
|
function GuidelyScreenScope({ name, children }) {
|
|
4759
|
-
return /* @__PURE__ */ (0,
|
|
6010
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(GuidelyScreenContext.Provider, { value: name, children });
|
|
4760
6011
|
}
|
|
4761
6012
|
function guidelyScreenLayout({ route, children }) {
|
|
4762
|
-
return /* @__PURE__ */ (0,
|
|
6013
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(GuidelyScreenScope, { name: route.name, children });
|
|
4763
6014
|
}
|
|
4764
|
-
var
|
|
6015
|
+
var TAP_SLOP2 = 10;
|
|
4765
6016
|
function useGuidelyTarget(id) {
|
|
4766
|
-
const client = (0,
|
|
4767
|
-
const screen = (0,
|
|
4768
|
-
const ref = (0,
|
|
4769
|
-
const start = (0,
|
|
4770
|
-
(0,
|
|
6017
|
+
const client = (0, import_react9.useContext)(GuidelyContext);
|
|
6018
|
+
const screen = (0, import_react9.useContext)(GuidelyScreenContext);
|
|
6019
|
+
const ref = (0, import_react9.useRef)(null);
|
|
6020
|
+
const start = (0, import_react9.useRef)(null);
|
|
6021
|
+
(0, import_react9.useEffect)(() => {
|
|
4771
6022
|
if (!client || !id) return;
|
|
4772
6023
|
return client.targets.register(id, {
|
|
4773
6024
|
screen,
|
|
@@ -4778,15 +6029,15 @@ function useGuidelyTarget(id) {
|
|
|
4778
6029
|
})
|
|
4779
6030
|
});
|
|
4780
6031
|
}, [client, id, screen]);
|
|
4781
|
-
const onLayout = (0,
|
|
4782
|
-
const onTouchStart = (0,
|
|
6032
|
+
const onLayout = (0, import_react9.useCallback)(() => client?.targets.changed(), [client]);
|
|
6033
|
+
const onTouchStart = (0, import_react9.useCallback)((e) => {
|
|
4783
6034
|
start.current = { x: e.nativeEvent.pageX, y: e.nativeEvent.pageY };
|
|
4784
6035
|
}, []);
|
|
4785
|
-
const onTouchEnd = (0,
|
|
6036
|
+
const onTouchEnd = (0, import_react9.useCallback)(
|
|
4786
6037
|
(e) => {
|
|
4787
6038
|
const s = start.current;
|
|
4788
6039
|
start.current = null;
|
|
4789
|
-
if (s && Math.abs(e.nativeEvent.pageX - s.x) <
|
|
6040
|
+
if (s && Math.abs(e.nativeEvent.pageX - s.x) < TAP_SLOP2 && Math.abs(e.nativeEvent.pageY - s.y) < TAP_SLOP2) client?.targets.tapped(id);
|
|
4790
6041
|
},
|
|
4791
6042
|
[client, id]
|
|
4792
6043
|
);
|
|
@@ -4794,8 +6045,8 @@ function useGuidelyTarget(id) {
|
|
|
4794
6045
|
}
|
|
4795
6046
|
function GuidelyTarget({ id, children, onLayout, onTouchStart, onTouchEnd, ...rest }) {
|
|
4796
6047
|
const t = useGuidelyTarget(id);
|
|
4797
|
-
return /* @__PURE__ */ (0,
|
|
4798
|
-
|
|
6048
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
6049
|
+
import_react_native12.View,
|
|
4799
6050
|
{
|
|
4800
6051
|
...rest,
|
|
4801
6052
|
ref: t.ref,
|
|
@@ -4819,7 +6070,7 @@ function GuidelyTarget({ id, children, onLayout, onTouchStart, onTouchEnd, ...re
|
|
|
4819
6070
|
|
|
4820
6071
|
// src/index.tsx
|
|
4821
6072
|
var import_flags6 = require("@guidelytech/flags");
|
|
4822
|
-
var
|
|
6073
|
+
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
4823
6074
|
function deviceLocale() {
|
|
4824
6075
|
try {
|
|
4825
6076
|
return Intl.DateTimeFormat().resolvedOptions().locale;
|
|
@@ -4828,33 +6079,34 @@ function deviceLocale() {
|
|
|
4828
6079
|
}
|
|
4829
6080
|
}
|
|
4830
6081
|
function defaultBridge(appVersion) {
|
|
4831
|
-
const { width, height } =
|
|
6082
|
+
const { width, height } = import_react_native13.Dimensions.get("window");
|
|
4832
6083
|
return {
|
|
4833
6084
|
device: {
|
|
4834
|
-
platform:
|
|
4835
|
-
deviceType:
|
|
6085
|
+
platform: import_react_native13.Platform.OS === "android" ? "android" : "ios",
|
|
6086
|
+
deviceType: import_react_native13.Platform.OS === "ios" && import_react_native13.Platform.isPad || Math.min(width, height) >= 600 ? "tablet" : "mobile",
|
|
4836
6087
|
locale: deviceLocale(),
|
|
4837
6088
|
appVersion
|
|
4838
6089
|
},
|
|
4839
|
-
openExternal: (url) => void
|
|
4840
|
-
})
|
|
6090
|
+
openExternal: (url) => void import_react_native13.Linking.openURL(url).catch(() => {
|
|
6091
|
+
}),
|
|
6092
|
+
windowSize: () => import_react_native13.Dimensions.get("window")
|
|
4841
6093
|
};
|
|
4842
6094
|
}
|
|
4843
6095
|
function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPayload, flagAttributes, ...options }) {
|
|
4844
|
-
const [client, setClient] = (0,
|
|
4845
|
-
const optionsRef = (0,
|
|
6096
|
+
const [client, setClient] = (0, import_react10.useState)(null);
|
|
6097
|
+
const optionsRef = (0, import_react10.useRef)(options);
|
|
4846
6098
|
optionsRef.current = options;
|
|
4847
|
-
const userRef = (0,
|
|
6099
|
+
const userRef = (0, import_react10.useRef)(user);
|
|
4848
6100
|
userRef.current = user;
|
|
4849
6101
|
const flagsOpts = options.flags === false ? null : options.flags ?? {};
|
|
4850
6102
|
const payload = flagsPayload ?? flagsOpts?.payload ?? null;
|
|
4851
|
-
const payloadRef = (0,
|
|
6103
|
+
const payloadRef = (0, import_react10.useRef)(payload);
|
|
4852
6104
|
payloadRef.current = payload;
|
|
4853
|
-
const appAttributes = (0,
|
|
4854
|
-
const appAttributesRef = (0,
|
|
6105
|
+
const appAttributes = (0, import_react10.useMemo)(() => ({ ...flagsOpts?.attributes ?? {}, ...flagAttributes ?? {} }), [(0, import_flags3.stableStringify)(flagsOpts?.attributes ?? {}), (0, import_flags3.stableStringify)(flagAttributes ?? {})]);
|
|
6106
|
+
const appAttributesRef = (0, import_react10.useRef)(appAttributes);
|
|
4855
6107
|
appAttributesRef.current = appAttributes;
|
|
4856
6108
|
const { projectId, environment, apiUrl, storage } = options;
|
|
4857
|
-
(0,
|
|
6109
|
+
(0, import_react10.useEffect)(() => {
|
|
4858
6110
|
if (!projectId) return;
|
|
4859
6111
|
let c;
|
|
4860
6112
|
try {
|
|
@@ -4880,7 +6132,7 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
|
|
|
4880
6132
|
}
|
|
4881
6133
|
setClient(c);
|
|
4882
6134
|
void c.init();
|
|
4883
|
-
const sub =
|
|
6135
|
+
const sub = import_react_native13.AppState.addEventListener("change", (s) => c.setAppState(s));
|
|
4884
6136
|
return () => {
|
|
4885
6137
|
sub.remove();
|
|
4886
6138
|
c.destroy();
|
|
@@ -4888,8 +6140,8 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
|
|
|
4888
6140
|
};
|
|
4889
6141
|
}, [projectId, environment, apiUrl, storage]);
|
|
4890
6142
|
const userKey = user ? JSON.stringify([user.id, user.traits ?? null, user.account ?? null]) : "";
|
|
4891
|
-
const hadUser = (0,
|
|
4892
|
-
(0,
|
|
6143
|
+
const hadUser = (0, import_react10.useRef)(false);
|
|
6144
|
+
(0, import_react10.useEffect)(() => {
|
|
4893
6145
|
if (!client) return;
|
|
4894
6146
|
if (user && user.id) {
|
|
4895
6147
|
hadUser.current = true;
|
|
@@ -4899,16 +6151,21 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
|
|
|
4899
6151
|
client.reset();
|
|
4900
6152
|
}
|
|
4901
6153
|
}, [client, userKey]);
|
|
4902
|
-
(0,
|
|
6154
|
+
(0, import_react10.useEffect)(() => {
|
|
4903
6155
|
client?.flags.setAttributes(appAttributes);
|
|
4904
6156
|
}, [client, appAttributes]);
|
|
6157
|
+
(0, import_react10.useEffect)(() => {
|
|
6158
|
+
client?.setLocale(options.locale ?? null);
|
|
6159
|
+
}, [client, options.locale]);
|
|
4905
6160
|
const bootstrap = useBootstrap(payload, { ...user?.id ? { ...user.traits ?? {}, id: user.id } : {}, ...appAttributes });
|
|
4906
|
-
const flags = (0,
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
6161
|
+
const flags = (0, import_react10.useMemo)(() => ({ client: client?.flags ?? null, bootstrap }), [client, bootstrap]);
|
|
6162
|
+
const tapObserver = useTapObserver(client);
|
|
6163
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(GuidelyContext.Provider, { value: client, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(FlagsContext.Provider, { value: flags, children: [
|
|
6164
|
+
options.autocapture === false ? children : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native13.View, { style: styles10.root, ...tapObserver, children }),
|
|
6165
|
+
client ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(GuidelyOverlay, { client, insets, launcherOffset }) : null
|
|
4910
6166
|
] }) });
|
|
4911
6167
|
}
|
|
6168
|
+
var styles10 = import_react_native13.StyleSheet.create({ root: { flex: 1 } });
|
|
4912
6169
|
var NOOP_RESULTS = {
|
|
4913
6170
|
init: Promise.resolve(),
|
|
4914
6171
|
ready: Promise.resolve(),
|
|
@@ -4957,12 +6214,12 @@ var noopClient = new Proxy({}, {
|
|
|
4957
6214
|
}
|
|
4958
6215
|
});
|
|
4959
6216
|
function useGuidely() {
|
|
4960
|
-
return (0,
|
|
6217
|
+
return (0, import_react10.useContext)(GuidelyContext) ?? noopClient;
|
|
4961
6218
|
}
|
|
4962
6219
|
function useGuidelyReady() {
|
|
4963
|
-
const client = (0,
|
|
4964
|
-
const [ready, setReady] = (0,
|
|
4965
|
-
(0,
|
|
6220
|
+
const client = (0, import_react10.useContext)(GuidelyContext);
|
|
6221
|
+
const [ready, setReady] = (0, import_react10.useState)(() => !!client?.isClientReady());
|
|
6222
|
+
(0, import_react10.useEffect)(() => {
|
|
4966
6223
|
if (!client) return setReady(false);
|
|
4967
6224
|
setReady(client.isClientReady());
|
|
4968
6225
|
const off = client.on("ready", () => setReady(true));
|
|
@@ -4972,44 +6229,44 @@ function useGuidelyReady() {
|
|
|
4972
6229
|
return ready;
|
|
4973
6230
|
}
|
|
4974
6231
|
function useGuidelyIdentify(userId, traits, account) {
|
|
4975
|
-
const client = (0,
|
|
6232
|
+
const client = (0, import_react10.useContext)(GuidelyContext);
|
|
4976
6233
|
const key = JSON.stringify([userId ?? null, traits ?? null, account ?? null]);
|
|
4977
|
-
(0,
|
|
6234
|
+
(0, import_react10.useEffect)(() => {
|
|
4978
6235
|
if (client && userId) void client.identify(userId, traits, account);
|
|
4979
6236
|
}, [client, key]);
|
|
4980
6237
|
}
|
|
4981
6238
|
function useGuidelyEvent(event, handler) {
|
|
4982
|
-
const client = (0,
|
|
4983
|
-
const ref = (0,
|
|
6239
|
+
const client = (0, import_react10.useContext)(GuidelyContext);
|
|
6240
|
+
const ref = (0, import_react10.useRef)(handler);
|
|
4984
6241
|
ref.current = handler;
|
|
4985
|
-
(0,
|
|
6242
|
+
(0, import_react10.useEffect)(() => {
|
|
4986
6243
|
if (!client) return;
|
|
4987
6244
|
return client.on(event, ((...args) => ref.current(...args)));
|
|
4988
6245
|
}, [client, event]);
|
|
4989
6246
|
}
|
|
4990
6247
|
function useGuidelyScreen(name, properties) {
|
|
4991
|
-
const client = (0,
|
|
4992
|
-
(0,
|
|
6248
|
+
const client = (0, import_react10.useContext)(GuidelyContext);
|
|
6249
|
+
(0, import_react10.useEffect)(() => {
|
|
4993
6250
|
if (client && name) client.screen(name, properties);
|
|
4994
6251
|
}, [client, name]);
|
|
4995
6252
|
}
|
|
4996
6253
|
function useGuidelyNavigation(navigationRef, options = {}) {
|
|
4997
|
-
const client = (0,
|
|
4998
|
-
const nameRef = (0,
|
|
6254
|
+
const client = (0, import_react10.useContext)(GuidelyContext);
|
|
6255
|
+
const nameRef = (0, import_react10.useRef)(options.screenName);
|
|
4999
6256
|
nameRef.current = options.screenName;
|
|
5000
6257
|
const report = () => {
|
|
5001
6258
|
const route = navigationRef.getCurrentRoute?.();
|
|
5002
6259
|
if (client && route?.name) client.screen(nameRef.current ? nameRef.current(route) : route.name);
|
|
5003
6260
|
};
|
|
5004
|
-
(0,
|
|
6261
|
+
(0, import_react10.useEffect)(report, [client]);
|
|
5005
6262
|
return { onReady: report, onStateChange: report };
|
|
5006
6263
|
}
|
|
5007
6264
|
function useUiSelector(select, fallback) {
|
|
5008
|
-
const client = (0,
|
|
5009
|
-
const selectRef = (0,
|
|
6265
|
+
const client = (0, import_react10.useContext)(GuidelyContext);
|
|
6266
|
+
const selectRef = (0, import_react10.useRef)(select);
|
|
5010
6267
|
selectRef.current = select;
|
|
5011
|
-
const [value, setValue] = (0,
|
|
5012
|
-
(0,
|
|
6268
|
+
const [value, setValue] = (0, import_react10.useState)(() => client ? select(client) : fallback);
|
|
6269
|
+
(0, import_react10.useEffect)(() => {
|
|
5013
6270
|
if (!client) return;
|
|
5014
6271
|
let last = JSON.stringify(selectRef.current(client));
|
|
5015
6272
|
setValue(selectRef.current(client));
|
|
@@ -5049,14 +6306,18 @@ function useActiveGuide() {
|
|
|
5049
6306
|
FeatureValue,
|
|
5050
6307
|
FlagsProvider,
|
|
5051
6308
|
GuidelyClient,
|
|
6309
|
+
GuidelyMask,
|
|
5052
6310
|
GuidelyOverlay,
|
|
5053
6311
|
GuidelyProvider,
|
|
5054
6312
|
GuidelyScreenScope,
|
|
5055
6313
|
GuidelyTarget,
|
|
6314
|
+
HeatmapBuffer,
|
|
5056
6315
|
InMemoryStickyBucketStore,
|
|
5057
6316
|
Messenger,
|
|
6317
|
+
RageClickDetector,
|
|
5058
6318
|
SDK_NAME,
|
|
5059
6319
|
SDK_VERSION,
|
|
6320
|
+
TapTracker,
|
|
5060
6321
|
TargetRegistry,
|
|
5061
6322
|
createFlagsClient,
|
|
5062
6323
|
createSseParser,
|