@lucida-analytics/react-native 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1765 @@
1
+ // src/client.ts
2
+ import { Core, flagEventProps, interventionHub, offlineTransport } from "@lucida-analytics/sdk-core";
3
+
4
+ // src/adapters.ts
5
+ var PERSISTED_KEYS = [
6
+ "lc_identity",
7
+ "lc_session",
8
+ "lc_props",
9
+ "lc_groups",
10
+ "lc_consent",
11
+ "lc_flags",
12
+ "lc_offline",
13
+ "lc_app_version",
14
+ "lc_guide_seen",
15
+ "lc_person_identified"
16
+ ];
17
+ function asyncStorageAdapter(as) {
18
+ const mem = /* @__PURE__ */ new Map();
19
+ const ready = as.multiGet(PERSISTED_KEYS).then(
20
+ (rows) => {
21
+ for (const [k, v] of rows) if (v !== null) mem.set(k, v);
22
+ },
23
+ () => void 0
24
+ // unreadable storage: start empty rather than fail init
25
+ );
26
+ const swallow = (p) => void p.catch(() => void 0);
27
+ return {
28
+ ready,
29
+ storage: {
30
+ get: (k) => mem.get(k) ?? null,
31
+ set(k, v) {
32
+ mem.set(k, v);
33
+ swallow(as.setItem(k, v));
34
+ },
35
+ remove(k) {
36
+ mem.delete(k);
37
+ swallow(as.removeItem(k));
38
+ }
39
+ }
40
+ };
41
+ }
42
+ function memoryStorage() {
43
+ const m = /* @__PURE__ */ new Map();
44
+ return { get: (k) => m.get(k) ?? null, set: (k, v) => void m.set(k, v), remove: (k) => void m.delete(k) };
45
+ }
46
+ function fetchTransport(fetchImpl, timeoutMs = 0) {
47
+ return {
48
+ async send(url, body) {
49
+ const ctl = timeoutMs > 0 && typeof AbortController !== "undefined" ? new AbortController() : void 0;
50
+ const timer = ctl ? setTimeout(() => ctl.abort(), timeoutMs) : void 0;
51
+ try {
52
+ const r = await fetchImpl(url, {
53
+ method: "POST",
54
+ body,
55
+ headers: { "Content-Type": "text/plain;charset=UTF-8" },
56
+ ...ctl ? { signal: ctl.signal } : {}
57
+ });
58
+ return { status: r.status };
59
+ } catch {
60
+ return { status: 0 };
61
+ } finally {
62
+ clearTimeout(timer);
63
+ }
64
+ }
65
+ };
66
+ }
67
+ function splitBatches(inner, max) {
68
+ return {
69
+ async send(url, body, opts) {
70
+ let parsed;
71
+ try {
72
+ parsed = JSON.parse(body);
73
+ } catch {
74
+ return inner.send(url, body, opts);
75
+ }
76
+ const batch = parsed.batch;
77
+ if (!Array.isArray(batch) || batch.length <= max) return inner.send(url, body, opts);
78
+ for (let i = 0; i < batch.length; i += max) {
79
+ const r = await inner.send(url, JSON.stringify({ ...parsed, batch: batch.slice(i, i + max) }), opts);
80
+ if (r.status < 200 || r.status >= 300) return r;
81
+ }
82
+ return { status: 200 };
83
+ }
84
+ };
85
+ }
86
+ function appLifecycle(app, h) {
87
+ let fg = app.currentState === "active";
88
+ const sub = app.addEventListener("change", (s) => {
89
+ if (s === "active" && !fg) h.onForeground();
90
+ else if (s !== "active" && fg) h.onBackground();
91
+ fg = s === "active";
92
+ });
93
+ return () => sub.remove();
94
+ }
95
+ function withTimeout(inner, ms) {
96
+ if (ms <= 0) return inner;
97
+ return {
98
+ send: (url, body, opts) => new Promise((resolve) => {
99
+ const timer = setTimeout(() => resolve({ status: 0 }), ms);
100
+ inner.send(url, body, opts).then(
101
+ (r) => (clearTimeout(timer), resolve(r)),
102
+ () => (clearTimeout(timer), resolve({ status: 0 }))
103
+ );
104
+ })
105
+ };
106
+ }
107
+ function encryptedStorage(inner, cipher, keys) {
108
+ const enc = new Set(keys);
109
+ return {
110
+ get: (k) => {
111
+ const v = inner.get(k);
112
+ return v !== null && enc.has(k) ? cipher.decrypt(v) : v;
113
+ },
114
+ set: (k, v) => inner.set(k, enc.has(k) ? cipher.encrypt(v) : v),
115
+ remove: (k) => inner.remove(k)
116
+ };
117
+ }
118
+
119
+ // src/autocapture.ts
120
+ function createAutocapture(core, config, now = Date.now) {
121
+ const taps = [];
122
+ const rc = { enabled: true, minTaps: 3, windowMs: 1e3, radius: 30, ...config.rageTap };
123
+ function emit(type, chain) {
124
+ if (!config.enabled || chain.some((e) => e.noCapture)) return void 0;
125
+ const ignore = new Set(config.ignoreLabels ?? []);
126
+ const allow = new Set(config.propsAllowList ?? []);
127
+ const kept = chain.filter((e) => !ignore.has(e.name) && !(e.label !== void 0 && ignore.has(e.label))).slice(0, config.maxElements ?? 10);
128
+ if (!kept.length) return void 0;
129
+ const elements = kept.map((el) => {
130
+ const o = { tag_name: el.name };
131
+ if (!el.isInput && el.label) o.text = el.label.slice(0, 255);
132
+ for (const [k, v] of Object.entries(el.props ?? {})) if (allow.has(k)) o[`attr__${k}`] = v;
133
+ return o;
134
+ });
135
+ const text = elements.find((e) => e.text !== void 0)?.text ?? "";
136
+ core.capture("$autocapture", { $event_type: type, $elements: elements, $el_text: text });
137
+ return elements[0];
138
+ }
139
+ return {
140
+ config,
141
+ tap(chain, x, y) {
142
+ const top = emit("touch", chain);
143
+ if (!top || !rc.enabled) return;
144
+ const t = now();
145
+ taps.push({ t, x, y });
146
+ while (taps.length && t - taps[0].t > rc.windowMs) taps.shift();
147
+ const near = taps.filter((p) => Math.hypot(p.x - x, p.y - y) <= rc.radius).length;
148
+ if (near >= rc.minTaps) {
149
+ taps.length = 0;
150
+ core.capture("$rageclick", { $event_type: "touch", $tap_count: near, $elements: [top] });
151
+ }
152
+ },
153
+ change(chain) {
154
+ emit("change", chain);
155
+ }
156
+ };
157
+ }
158
+ function touchHandler(ac) {
159
+ return (e) => {
160
+ if (!ac.config.enabled) return;
161
+ const chain = chainFromFiber(e._targetInst, ac.config);
162
+ if (chain.length) ac.tap(chain, e.nativeEvent?.pageX ?? 0, e.nativeEvent?.pageY ?? 0);
163
+ };
164
+ }
165
+ var INPUT_NAMES = /^(TextInput|RCTSinglelineTextInputView|RCTMultilineTextInputView|AndroidTextInput)$/;
166
+ function nameOf(f) {
167
+ const t = f.type;
168
+ if (typeof t === "string") return t;
169
+ return t?.displayName ?? t?.name ?? void 0;
170
+ }
171
+ function chainFromFiber(start, cfg) {
172
+ const noCapture = cfg.noCaptureProp ?? "lucidaNoCapture";
173
+ const labelProp = cfg.customLabelProp ?? "testID";
174
+ const out = [];
175
+ for (let f = start; f; f = f.return) {
176
+ const p = f.memoizedProps ?? {};
177
+ const name = nameOf(f);
178
+ if (!name) continue;
179
+ const input = INPUT_NAMES.test(name);
180
+ const label = typeof p.accessibilityLabel === "string" ? p.accessibilityLabel : typeof p[labelProp] === "string" ? p[labelProp] : void 0;
181
+ const flagged = p[noCapture] === true;
182
+ if (!input && label === void 0 && !flagged) continue;
183
+ const props = {};
184
+ for (const [k, v] of Object.entries(p)) if (typeof v === "string" && k !== "children") props[k] = v;
185
+ out.push({ name, label, noCapture: flagged, isInput: input, props });
186
+ }
187
+ return out;
188
+ }
189
+
190
+ // src/install.ts
191
+ var CLICK_IDS = ["gclid", "gbraid", "wbraid", "fbclid", "ttclid", "msclkid"];
192
+ var UTM = ["source", "medium", "campaign", "term", "content"];
193
+ var ASA_URL = "https://api-adservices.apple.com/api/v1/";
194
+ function installReferrerProps(raw, clickTs, beginTs) {
195
+ const q = /* @__PURE__ */ new Map();
196
+ for (const part of raw.split("&")) {
197
+ if (!part) continue;
198
+ const i = part.indexOf("=");
199
+ const k = i < 0 ? part : part.slice(0, i);
200
+ let v = i < 0 ? "" : part.slice(i + 1);
201
+ try {
202
+ v = decodeURIComponent(v.replace(/\+/g, " "));
203
+ } catch {
204
+ }
205
+ q.set(k, v);
206
+ }
207
+ const out = { $install_store: "google_play", $install_referrer: raw };
208
+ if (clickTs !== void 0) out.$install_referrer_click_ts = clickTs;
209
+ if (beginTs !== void 0) out.$install_begin_ts = beginTs;
210
+ for (const u of UTM) if (q.get(`utm_${u}`)) out[`$utm_${u}`] = q.get(`utm_${u}`);
211
+ for (const c of CLICK_IDS) if (q.get(c)) out[`$${c}`] = q.get(c);
212
+ return out;
213
+ }
214
+ function appStoreInstallProps(json) {
215
+ const out = { $install_store: "app_store" };
216
+ if (!json || typeof json !== "object") return out;
217
+ const o = json;
218
+ out.$install_adservices_attribution = o.attribution === true;
219
+ if (o.attribution !== true) return out;
220
+ out.$utm_source = "apple_search_ads";
221
+ out.$utm_medium = "cpc";
222
+ const map = [
223
+ ["campaignId", "$asa_campaign_id", "$utm_campaign"],
224
+ ["keywordId", "$asa_keyword_id", "$utm_term"],
225
+ ["adGroupId", "$asa_ad_group_id", "$utm_content"],
226
+ ["orgId", "$asa_org_id"],
227
+ ["adId", "$asa_ad_id"],
228
+ ["conversionType", "$asa_conversion_type"]
229
+ ];
230
+ for (const [src, asa, utm] of map) {
231
+ const v = o[src];
232
+ if (v === void 0 || v === null || v === "") continue;
233
+ out[asa] = String(v);
234
+ if (utm) out[utm] = String(v);
235
+ }
236
+ return out;
237
+ }
238
+ async function resolveInstallProps(native, d) {
239
+ let fallback = {};
240
+ try {
241
+ const info = await native();
242
+ if (!info) return {};
243
+ fallback = { $install_store: info.platform === "android" ? "google_play" : "app_store" };
244
+ if (info.platform === "android") {
245
+ return info.referrer ? installReferrerProps(info.referrer, info.clickTs, info.beginTs) : { $install_store: "google_play" };
246
+ }
247
+ if (!info.token) return appStoreInstallProps(null);
248
+ const attempts = d.attempts ?? 3;
249
+ for (let i = 0; i < attempts; i++) {
250
+ const ctl = typeof AbortController === "undefined" ? void 0 : new AbortController();
251
+ const timer = ctl ? setTimeout(() => ctl.abort(), d.timeoutMs ?? 1e4) : void 0;
252
+ try {
253
+ const r = await d.fetch(ASA_URL, { method: "POST", headers: { "Content-Type": "text/plain" }, body: info.token, signal: ctl?.signal ?? null });
254
+ if (r.ok) return appStoreInstallProps(await r.json());
255
+ if (r.status !== 404) break;
256
+ } finally {
257
+ if (timer) clearTimeout(timer);
258
+ }
259
+ if (i + 1 < attempts) await new Promise((res) => setTimeout(res, d.retryDelayMs ?? 5e3));
260
+ }
261
+ return appStoreInstallProps(null);
262
+ } catch {
263
+ return fallback;
264
+ }
265
+ }
266
+
267
+ // src/policy.ts
268
+ function runHooks(hooks, event, log) {
269
+ let cur = event;
270
+ for (const h of hooks) {
271
+ try {
272
+ const next = h(cur);
273
+ if (next === null || next === void 0) return null;
274
+ cur = next;
275
+ } catch (e) {
276
+ log("beforeSend hook threw, keeping event:", e);
277
+ }
278
+ }
279
+ return cur;
280
+ }
281
+ function sampleHook(rate, random = Math.random) {
282
+ return (e) => {
283
+ if (rate >= 1) return e;
284
+ if (rate <= 0 || random() >= rate) return null;
285
+ return { ...e, properties: { ...e.properties, $sample_rate: rate } };
286
+ };
287
+ }
288
+ var KEY = "lc_person_identified";
289
+ var ALWAYS_PROCESSED = /* @__PURE__ */ new Set(["$identify", "$create_alias", "$groupidentify"]);
290
+ var PersonMode = class {
291
+ constructor(mode, storage) {
292
+ this.mode = mode;
293
+ this.storage = storage;
294
+ this.identified = storage.get(KEY) === "1";
295
+ }
296
+ mode;
297
+ storage;
298
+ identified;
299
+ get blocksPersonWrites() {
300
+ return this.mode === "never";
301
+ }
302
+ markIdentified() {
303
+ if (this.mode !== "identified_only" || this.identified) return;
304
+ this.identified = true;
305
+ this.storage.set(KEY, "1");
306
+ }
307
+ /** True when an event of this name should carry `$process_person_profile: false`. */
308
+ personless(event) {
309
+ if (this.mode === "always") return false;
310
+ if (this.mode === "never") return !ALWAYS_PROCESSED.has(event);
311
+ return !this.identified && !ALWAYS_PROCESSED.has(event);
312
+ }
313
+ reset() {
314
+ this.identified = false;
315
+ this.storage.remove(KEY);
316
+ }
317
+ };
318
+ function applyPolicy(person, o, e) {
319
+ const writes = !!(e.$set || e.$set_once || e.$unset);
320
+ if (writes || ALWAYS_PROCESSED.has(e.event)) person.markIdentified();
321
+ let out = e;
322
+ if (person.blocksPersonWrites && writes) {
323
+ o.log("personProfiles=never: ignoring $set/$set_once on", e.event);
324
+ const { $set: _s, $set_once: _o, $unset: _u, ...rest } = e;
325
+ out = rest;
326
+ }
327
+ const extra = {};
328
+ if (person.personless(e.event)) extra.$process_person_profile = false;
329
+ if (o.disableGeoip) extra.$geoip_disable = true;
330
+ if (Object.keys(extra).length) out = { ...out, properties: { ...out.properties, ...extra } };
331
+ return o.beforeSend?.length ? runHooks(o.beforeSend, out, o.log) : out;
332
+ }
333
+ function installPolicy(core, o) {
334
+ const person = new PersonMode(o.personProfiles ?? "always", core.storage);
335
+ core.setFilter((e) => applyPolicy(person, o, e));
336
+ return person;
337
+ }
338
+
339
+ // src/host-headers.ts
340
+ function hostMatches(hosts, host) {
341
+ return hosts.some((h) => {
342
+ const x = h.toLowerCase();
343
+ return x.startsWith(".") ? host.endsWith(x) : host === x;
344
+ });
345
+ }
346
+ var hostOf = (url) => /^[a-z][a-z0-9+.-]*:\/\/([^/:?#]+)/i.exec(url)?.[1]?.toLowerCase();
347
+ function tracingHeaders(hosts, apiHost, consent, ids) {
348
+ const api = hostOf(apiHost);
349
+ return (url, baggage) => {
350
+ const host = hostOf(url);
351
+ if (!host || !hosts.length || host === api || !hostMatches(hosts, host) || !consent()) return {};
352
+ const i = ids();
353
+ return { "X-Lucida-Distinct-Id": i.distinctId, "X-Lucida-Session-Id": i.sessionId, baggage: withBaggage(baggage, i.sessionId) };
354
+ };
355
+ }
356
+ function withBaggage(existing, sessionId) {
357
+ const kept = (existing ?? "").split(",").map((s) => s.trim()).filter((s) => s && !s.startsWith("lucida.session_id="));
358
+ return [...kept, `lucida.session_id=${sessionId}`].join(",");
359
+ }
360
+ function tracedFetch(fetchImpl, headersFor) {
361
+ return ((input, init) => {
362
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
363
+ const headers = new Headers(init?.headers ?? (typeof input === "object" && "headers" in input ? input.headers : void 0));
364
+ const extra = headersFor(url, headers.get("baggage"));
365
+ if (!Object.keys(extra).length) return fetchImpl(input, init);
366
+ for (const [k, v] of Object.entries(extra)) headers.set(k, v);
367
+ return fetchImpl(input, { ...init, headers });
368
+ });
369
+ }
370
+
371
+ // src/flags.ts
372
+ import { FlagFreshness, evaluateAll, flagResult, requiresServer, servable, startFlagSync } from "@lucida-analytics/sdk-core";
373
+ var CACHE_KEY = "lc_flags";
374
+ var norm = (v) => typeof v === "object" ? v.value ?? v.variant ?? !!v.enabled : v;
375
+ function createFlags(d) {
376
+ const { core } = d;
377
+ let values = {};
378
+ let payloads = {};
379
+ let reasons = {};
380
+ const fresh = new FlagFreshness(d.options?.cacheTtlMs);
381
+ let requestId;
382
+ let loaded = false;
383
+ let seq = 0;
384
+ const called = /* @__PURE__ */ new Set();
385
+ const exposed = /* @__PURE__ */ new Set();
386
+ let calledSession = "";
387
+ const subs = /* @__PURE__ */ new Set();
388
+ let overrides = {};
389
+ const merged = () => ({ ...values, ...overrides });
390
+ const apply = (v, p) => {
391
+ values = v;
392
+ payloads = p;
393
+ loaded = true;
394
+ for (const s of subs) s(merged());
395
+ };
396
+ try {
397
+ const c = JSON.parse(core.storage.get(CACHE_KEY) ?? "null");
398
+ if (c) {
399
+ ({ values, payloads } = c);
400
+ reasons = Object.fromEntries(Object.keys(values).map((k) => [k, "cached"]));
401
+ fresh.restore(c);
402
+ }
403
+ } catch {
404
+ }
405
+ if (d.options?.bootstrap) {
406
+ values = { ...values, ...d.options.bootstrap.flags };
407
+ payloads = { ...payloads, ...d.options.bootstrap.payloads };
408
+ for (const k of Object.keys(d.options.bootstrap.flags ?? {})) reasons[k] = "bootstrap";
409
+ }
410
+ let personProps = {};
411
+ let groupProps = {};
412
+ const contexts = d.options?.evaluationContexts ?? [];
413
+ const appContexts = contexts.length ? { app_contexts: contexts } : {};
414
+ const flagPersonProps = () => ({ ...d.defaultProps, ...personProps });
415
+ const sendEvents = d.options?.sendFeatureFlagEvent !== false;
416
+ let localV = {};
417
+ const evalLocal = () => {
418
+ if (!d.localFlags?.length) return true;
419
+ const res = evaluateAll(d.localFlags, {
420
+ distinct_id: core.distinctId,
421
+ ...core.groups ? { groups: core.groups } : {},
422
+ ...Object.keys(groupProps).length ? { group_properties: groupProps } : {},
423
+ ...appContexts,
424
+ ...d.localFlagsContext,
425
+ person_properties: { ...flagPersonProps(), ...d.localFlagsContext?.person_properties }
426
+ });
427
+ const v = {};
428
+ const p = {};
429
+ let server = false;
430
+ for (const [k, r] of Object.entries(res)) {
431
+ if (requiresServer(r)) server = true;
432
+ else if (servable(r)) {
433
+ v[k] = r.value;
434
+ reasons[k] = r.reason;
435
+ if (r.payload !== void 0) p[k] = r.payload;
436
+ }
437
+ }
438
+ localV = v;
439
+ apply({ ...values, ...v }, { ...payloads, ...p });
440
+ return server;
441
+ };
442
+ const reload = (force = true) => {
443
+ const mine = ++seq;
444
+ if (!evalLocal() || d.disableRemote) return Promise.resolve();
445
+ if (!force && fresh.fresh(core.distinctId)) return Promise.resolve();
446
+ return new Promise((resolve) => {
447
+ d.whenConsented(() => {
448
+ if (mine !== seq) return resolve();
449
+ d.fetch(`${d.apiHost}/v1/flags`, {
450
+ method: "POST",
451
+ headers: { "Content-Type": "text/plain;charset=UTF-8" },
452
+ body: JSON.stringify({
453
+ api_key: d.apiKey,
454
+ ...core.distinctId ? { distinct_id: core.distinctId } : {},
455
+ ...core.anonDistinctId ? { anon_distinct_id: core.anonDistinctId } : {},
456
+ groups: core.groups ?? {},
457
+ person_properties: flagPersonProps(),
458
+ ...Object.keys(groupProps).length ? { group_properties: groupProps } : {},
459
+ ...d.defaultProps?.$app_version ? { app_version: d.defaultProps.$app_version } : {},
460
+ ...appContexts
461
+ })
462
+ }).then((r) => r.ok ? r.json() : Promise.reject(new Error(`status ${r.status}`))).then((j) => {
463
+ if (mine !== seq || j.quota_limited) return;
464
+ const v = {};
465
+ const p = {};
466
+ for (const [k, x] of Object.entries(j.flags ?? {})) {
467
+ v[k] = norm(x);
468
+ if (typeof x === "object" && x.payload !== void 0) p[k] = x.payload;
469
+ reasons[k] = typeof x === "object" && typeof x.reason === "string" ? x.reason : "remote";
470
+ }
471
+ requestId = j.request_id;
472
+ apply({ ...localV, ...v }, p);
473
+ core.storage.set(CACHE_KEY, JSON.stringify({ values: v, payloads: p, ...fresh.mark(core.distinctId) }));
474
+ }).catch(() => void 0).finally(resolve);
475
+ });
476
+ });
477
+ };
478
+ if (d.options?.preload !== false) {
479
+ core.onChange((c) => {
480
+ if (c === "reset") personProps = {};
481
+ if (c === "identify" || c === "reset" || c === "group") void reload();
482
+ });
483
+ void reload(false);
484
+ }
485
+ let stopSync;
486
+ const fs = d.flagStream;
487
+ if (fs && !d.disableRemote) {
488
+ const app = d.appState;
489
+ d.whenConsented(() => {
490
+ stopSync = startFlagSync({
491
+ apiHost: d.apiHost,
492
+ apiKey: d.apiKey,
493
+ fetch: d.fetch,
494
+ reload: () => void reload(),
495
+ ...typeof fs === "object" && fs.pollMs !== void 0 ? { pollMs: fs.pollMs } : {},
496
+ ...app ? {
497
+ visibility: {
498
+ hidden: () => app.currentState !== void 0 && app.currentState !== "active",
499
+ subscribe: (cb) => {
500
+ const sub = app.addEventListener("change", cb);
501
+ return () => sub.remove();
502
+ }
503
+ }
504
+ } : {}
505
+ });
506
+ });
507
+ }
508
+ return {
509
+ stop: () => stopSync?.(),
510
+ get(key) {
511
+ const v = overrides[key] ?? values[key];
512
+ if (v === void 0) return void 0;
513
+ if (key in overrides) return v;
514
+ const sid = core.sessionId;
515
+ if (sid !== calledSession) {
516
+ called.clear();
517
+ exposed.clear();
518
+ calledSession = sid;
519
+ }
520
+ if (!sendEvents) return v;
521
+ if (!exposed.has(key)) {
522
+ exposed.add(key);
523
+ core.capture("$exposure", {
524
+ $feature_flag: key,
525
+ $feature_flag_response: v,
526
+ ...typeof v === "string" ? { $feature_flag_variant: v } : {},
527
+ ...requestId ? { $feature_flag_request_id: requestId } : {}
528
+ });
529
+ }
530
+ const dedupe = `${key}:${String(v)}`;
531
+ if (!called.has(dedupe)) {
532
+ called.add(dedupe);
533
+ core.capture("$feature_flag_called", {
534
+ $feature_flag: key,
535
+ $feature_flag_response: v,
536
+ ...requestId ? { $feature_flag_request_id: requestId } : {}
537
+ });
538
+ }
539
+ return v;
540
+ },
541
+ payload: (key) => payloads[key],
542
+ result(key, o) {
543
+ const v = o?.sendExposure === false ? overrides[key] ?? values[key] : this.get(key);
544
+ return flagResult(key, v, payloads[key], key in overrides ? "override" : reasons[key] ?? "remote");
545
+ },
546
+ update(flags, p = {}, o = {}) {
547
+ for (const k of Object.keys(flags)) reasons[k] = "pushed";
548
+ apply(o.merge ? { ...values, ...flags } : { ...flags }, o.merge ? { ...payloads, ...p } : { ...p });
549
+ core.storage.set(CACHE_KEY, JSON.stringify({ values, payloads }));
550
+ },
551
+ async setPersonProperties(props, doReload = true) {
552
+ fresh.clear();
553
+ personProps = { ...personProps, ...props };
554
+ if (doReload) await reload();
555
+ },
556
+ async resetPersonProperties(doReload = true) {
557
+ personProps = {};
558
+ if (doReload) await reload();
559
+ },
560
+ async setGroupProperties(type, props, doReload = true) {
561
+ fresh.clear();
562
+ groupProps = { ...groupProps, [type]: { ...groupProps[type], ...props } };
563
+ if (doReload) await reload();
564
+ },
565
+ async resetGroupProperties(doReload = true) {
566
+ groupProps = {};
567
+ if (doReload) await reload();
568
+ },
569
+ onChange(cb) {
570
+ subs.add(cb);
571
+ if (loaded) cb(merged());
572
+ return () => void subs.delete(cb);
573
+ },
574
+ reload,
575
+ all: () => merged(),
576
+ override(map) {
577
+ overrides = map ? { ...map } : {};
578
+ for (const s of subs) s(merged());
579
+ }
580
+ };
581
+ }
582
+
583
+ // src/guides.ts
584
+ var KINDS = ["modal", "banner", "announcement", "slideout", "tour", "carousel", "tooltip", "hotspot"];
585
+ var ANCHORABLE = ["tooltip", "hotspot", "tour"];
586
+ var HANDLED = ["url", "url_match", "flag_key", "sample_pct", "devices", "seen_guides", "not_seen_guides", "delay_seconds", "goal", "exclude_url_patterns"];
587
+ var SEEN_KEY = "lc_guide_seen";
588
+ function matches(screen, pattern, how) {
589
+ if (how === "exact") return screen === pattern;
590
+ if (how === "regex") {
591
+ try {
592
+ return new RegExp(pattern).test(screen);
593
+ } catch {
594
+ return false;
595
+ }
596
+ }
597
+ return screen.includes(pattern);
598
+ }
599
+ function guideButtons(g, i) {
600
+ const b = g.steps[i]?.buttons;
601
+ if (b && b.length) return b;
602
+ if (g.kind === "banner" || g.kind === "announcement") return [{ label: "Dismiss", action: "dismiss" }];
603
+ if (g.kind === "carousel") {
604
+ const last = i === g.steps.length - 1;
605
+ return [...i > 0 ? [{ label: "Back", action: "back" }] : [], { label: last ? "Done" : "Next", action: "next" }, { label: "Dismiss", action: "dismiss" }];
606
+ }
607
+ return [{ label: i === g.steps.length - 1 ? "Done" : "Next", action: "next" }, { label: "Dismiss", action: "dismiss" }];
608
+ }
609
+ function screenExcluded(screen, patterns) {
610
+ return patterns.some((p) => !!p && new RegExp("^" + p.split("*").map((x) => x.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$").test(screen));
611
+ }
612
+ function guideAnchor(g, i) {
613
+ const s = g.steps[i];
614
+ const id = s?.native_id;
615
+ if (!s || !id || !ANCHORABLE.includes(g.kind)) return void 0;
616
+ const wait = Math.min(Math.max(s.wait_ms ?? 3e3, 0), 15e3);
617
+ return { nativeId: id, ...s.placement ? { placement: s.placement } : {}, waitMs: wait, hotspot: g.kind === "hotspot" };
618
+ }
619
+ var unset = (v) => v == null || v === "" || Array.isArray(v) && v.length === 0;
620
+ var strs = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
621
+ function createGuides(d) {
622
+ const { core } = d;
623
+ const random = d.random ?? Math.random;
624
+ let guides = [];
625
+ let seen = /* @__PURE__ */ new Set();
626
+ try {
627
+ seen = new Set(strs(JSON.parse(core.storage.get(SEEN_KEY) ?? "[]")));
628
+ } catch {
629
+ }
630
+ const previewed = /* @__PURE__ */ new Set();
631
+ const subs = /* @__PURE__ */ new Set();
632
+ let view = null;
633
+ let screen;
634
+ let open = () => void 0;
635
+ const set = (v) => {
636
+ view = v;
637
+ for (const s of subs) s(v);
638
+ };
639
+ const props = (g, extra = {}) => ({ ...extra, $guide_id: g.id, $guide_kind: g.kind });
640
+ const eligible = (g, name) => {
641
+ if (!KINDS.includes(g.kind) || !g.steps?.length) return false;
642
+ if ((g.kind === "tooltip" || g.kind === "hotspot") && !g.steps[0]?.native_id) return false;
643
+ if (g.preview) return !previewed.has(g.id);
644
+ const c = g.conditions ?? {};
645
+ if (seen.has(g.id) || !Object.keys(c).every((k) => unset(c[k]) || HANDLED.includes(k))) return false;
646
+ const url = c["url"];
647
+ if (typeof url === "string" && url && !matches(name, url, typeof c["url_match"] === "string" ? c["url_match"] : "contains")) return false;
648
+ const f = c["flag_key"];
649
+ if (typeof f === "string" && f && !d.flag(f)) return false;
650
+ if (screenExcluded(name, strs(c["exclude_url_patterns"]))) return false;
651
+ const pct = c["sample_pct"];
652
+ if (!g.queued && typeof pct === "number" && random() * 100 >= pct) return false;
653
+ if (!unset(c["devices"]) && !strs(c["devices"]).some((x) => x === "mobile" || x === "tablet")) return false;
654
+ if (!unset(c["seen_guides"]) && !strs(c["seen_guides"]).every((x) => seen.has(x))) return false;
655
+ if (!unset(c["not_seen_guides"]) && strs(c["not_seen_guides"]).some((x) => seen.has(x))) return false;
656
+ return true;
657
+ };
658
+ const markSeen = (id) => {
659
+ seen.add(id);
660
+ core.storage.set(SEEN_KEY, JSON.stringify([...seen].sort()));
661
+ };
662
+ const present = (g, i) => {
663
+ const anchor = guideAnchor(g, i);
664
+ set({ guide: g, step: i, buttons: guideButtons(g, i), ...anchor ? { anchor } : {} });
665
+ };
666
+ const finish = () => {
667
+ set(null);
668
+ if (screen !== void 0) svc.show(screen);
669
+ };
670
+ const svc = {
671
+ async reload() {
672
+ if (!core.hasConsent("analytics")) return;
673
+ try {
674
+ const q = new URLSearchParams({ api_key: d.apiKey, distinct_id: core.distinctId });
675
+ const r = await d.fetch(`${d.apiHost}/v1/guides?${q}`);
676
+ if (!r.ok) return;
677
+ const b = await r.json();
678
+ guides = Array.isArray(b.guides) ? b.guides : [];
679
+ } catch {
680
+ return;
681
+ }
682
+ if (screen !== void 0) svc.show(screen);
683
+ },
684
+ show(name) {
685
+ screen = name;
686
+ if (view || !subs.size) return;
687
+ const g = [...guides.filter((x) => x.queued), ...guides.filter((x) => !x.queued)].find((x) => eligible(x, name));
688
+ if (!g) return;
689
+ if (g.preview) previewed.add(g.id);
690
+ else {
691
+ markSeen(g.id);
692
+ void core.capture("$guide_shown", props(g));
693
+ }
694
+ present(g, 0);
695
+ },
696
+ current: () => view,
697
+ subscribe(cb) {
698
+ subs.add(cb);
699
+ if (!view && screen !== void 0) svc.show(screen);
700
+ return () => void subs.delete(cb);
701
+ },
702
+ setOpener(fn) {
703
+ open = fn;
704
+ },
705
+ targetMissing() {
706
+ if (!view?.anchor || view.guide.preview) return;
707
+ const { guide: g, step: i, anchor } = view;
708
+ const id = g.steps[i].id;
709
+ void core.capture("$guide_target_missing", props(g, { guide_id: g.id, step_id: id, $guide_step_id: id, selector: anchor.nativeId }));
710
+ set({ guide: g, step: i, buttons: view.buttons });
711
+ },
712
+ act(b) {
713
+ if (!view) return;
714
+ const { guide: g, step: i } = view;
715
+ const quiet = !!g.preview;
716
+ const step = g.steps[i];
717
+ const sp = { $guide_step_id: step.id, $guide_step_index: i };
718
+ const cap = (e, p) => {
719
+ if (!quiet) void core.capture(e, p);
720
+ };
721
+ if (!b || b.action === "dismiss") {
722
+ cap("$guide_dismissed", props(g, sp));
723
+ return finish();
724
+ }
725
+ if (b.action === "back") return present(g, Math.max(i - 1, 0));
726
+ if (b.action === "link" && b.url && /^https:\/\//i.test(b.url)) open(b.url);
727
+ else if (b.action === "event" && b.event) cap(b.event, { $guide_id: g.id });
728
+ cap("$guide_step", props(g, sp));
729
+ if (i + 1 < g.steps.length && b.action !== "link") return present(g, i + 1);
730
+ cap("$guide_completed", props(g));
731
+ finish();
732
+ }
733
+ };
734
+ return svc;
735
+ }
736
+
737
+ // src/replay.ts
738
+ import { sha256Hex as sha256Hex2 } from "@lucida-analytics/sdk-core";
739
+
740
+ // src/dead-swipe.ts
741
+ var DEAD_SWIPE_MIN_DISTANCE = 48;
742
+ var DEAD_SWIPE_SETTLE_MS = 700;
743
+ var DEAD_SWIPE_COOLDOWN_MS = 2e3;
744
+ var swipeDirection = (dx, dy) => Math.abs(dx) >= Math.abs(dy) ? dx < 0 ? "left" : "right" : dy < 0 ? "up" : "down";
745
+ function deadSwipeDecision(i) {
746
+ const distance = Math.hypot(i.dx, i.dy);
747
+ if (distance < DEAD_SWIPE_MIN_DISTANCE || i.screen !== i.startScreen || i.startFingerprint !== i.endFingerprint) return null;
748
+ if (i.lastAt !== void 0 && i.nowMs - i.lastAt < DEAD_SWIPE_COOLDOWN_MS) return null;
749
+ return { $screen_name: i.screen, $swipe_direction: swipeDirection(i.dx, i.dy), $swipe_distance: Math.round(distance) };
750
+ }
751
+
752
+ // src/replay-hit.ts
753
+ var px = (style, prop) => {
754
+ const m = new RegExp(`(?:^|;)\\s*${prop}:\\s*(-?[\\d.]+)px`).exec(style);
755
+ return m ? Number(m[1]) : void 0;
756
+ };
757
+ function hitTest(root, x, y, fallback) {
758
+ const visit = (n, ox, oy, isRoot) => {
759
+ if (n.type !== void 0 && n.type !== 0 && n.type !== 2) return void 0;
760
+ const style = typeof n.attributes?.style === "string" ? n.attributes.style : "";
761
+ const left = ox + (px(style, "left") ?? 0);
762
+ const top = oy + (px(style, "top") ?? 0);
763
+ const w = px(style, "width");
764
+ const h = px(style, "height");
765
+ if (!isRoot && w !== void 0 && h !== void 0 && (x < left || y < top || x >= left + w || y >= top + h)) return void 0;
766
+ const kids = n.childNodes ?? [];
767
+ for (let i = kids.length - 1; i >= 0; i--) {
768
+ const hit = visit(kids[i], left, top, false);
769
+ if (hit !== void 0) return hit;
770
+ }
771
+ return n.type === 0 || n.tagName === "html" || n.id === void 0 ? void 0 : n.id;
772
+ };
773
+ if (typeof root !== "object" || root === null) return fallback;
774
+ return visit(root, 0, 0, true) ?? fallback;
775
+ }
776
+
777
+ // src/replay-masking.ts
778
+ var escape = (s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
779
+ function screenMasked(screen, patterns) {
780
+ return patterns.some((p) => new RegExp(`^${p.split("*").map(escape).join(".*")}$`, "is").test(screen));
781
+ }
782
+ function compilePatterns(patterns) {
783
+ const out = [];
784
+ for (const p of new Set(patterns)) {
785
+ if (!p) continue;
786
+ try {
787
+ out.push(new RegExp(p, "g"));
788
+ } catch {
789
+ }
790
+ }
791
+ return out;
792
+ }
793
+ function maskPatterns(text, patterns) {
794
+ if (patterns.length === 0 || text === "") return text;
795
+ const hit = new Array(text.length).fill(false);
796
+ let any = false;
797
+ for (const p of patterns) {
798
+ const re = new RegExp(p.source, p.flags.includes("g") ? p.flags : `${p.flags}g`);
799
+ let m;
800
+ while ((m = re.exec(text)) !== null) {
801
+ for (let i = m.index; i < m.index + m[0].length; i++) {
802
+ hit[i] = true;
803
+ any = true;
804
+ }
805
+ if (m[0].length === 0) re.lastIndex++;
806
+ }
807
+ }
808
+ if (!any) return text;
809
+ let out = "";
810
+ for (let i = 0; i < text.length; i++) {
811
+ const c = text[i];
812
+ out += hit[i] && !/\s/.test(c) ? "*" : c;
813
+ }
814
+ return out;
815
+ }
816
+ function maskSnapshotText(node, patterns) {
817
+ if (patterns.length === 0 || typeof node !== "object" || node === null) return node;
818
+ const n = node;
819
+ const out = { ...n };
820
+ if (typeof n.textContent === "string") out.textContent = maskPatterns(n.textContent, patterns);
821
+ if (n.attributes && typeof n.attributes.value === "string") out.attributes = { ...n.attributes, value: maskPatterns(n.attributes.value, patterns) };
822
+ if (Array.isArray(n.childNodes)) out.childNodes = n.childNodes.map((c) => maskSnapshotText(c, patterns));
823
+ return out;
824
+ }
825
+
826
+ // src/sensitive.ts
827
+ import { sha256Hex } from "@lucida-analytics/sdk-core";
828
+ var SENSITIVE_MAX_PLAIN_BYTES = 190;
829
+ var utf8Len = (cp) => cp < 128 ? 1 : cp < 2048 ? 2 : cp < 65536 ? 3 : 4;
830
+ function truncateUtf8(value, max = SENSITIVE_MAX_PLAIN_BYTES) {
831
+ let bytes = 0;
832
+ let text = "";
833
+ for (const ch of value) {
834
+ const n = utf8Len(ch.codePointAt(0));
835
+ if (bytes + n > max) return { text, truncated: true };
836
+ bytes += n;
837
+ text += ch;
838
+ }
839
+ return { text, truncated: false };
840
+ }
841
+ function encryptSensitive(module, key, plain) {
842
+ const fn = module?.encryptSensitive;
843
+ if (typeof fn !== "function") return null;
844
+ try {
845
+ const out = fn.call(module, key.pkcs1, key.n, key.e, plain);
846
+ return typeof out === "string" && out !== "" ? out : null;
847
+ } catch {
848
+ return null;
849
+ }
850
+ }
851
+ function createSensitiveCapture(module) {
852
+ const seen = /* @__PURE__ */ new Set();
853
+ let session;
854
+ return {
855
+ properties(field, value, screen, key, sessionId) {
856
+ if (session !== sessionId) {
857
+ seen.clear();
858
+ session = sessionId;
859
+ }
860
+ const digest = sha256Hex(`${field}\0${value}`);
861
+ if (seen.has(digest)) return null;
862
+ const t = truncateUtf8(value);
863
+ const ct = encryptSensitive(module, key, t.text);
864
+ if (ct === null) return null;
865
+ seen.add(digest);
866
+ return {
867
+ $exposure_field: field,
868
+ $exposure_kid: key.kid,
869
+ $exposure_ciphertext: ct,
870
+ $screen_name: screen,
871
+ ...t.truncated ? { $exposure_truncated: true } : {}
872
+ };
873
+ }
874
+ };
875
+ }
876
+
877
+ // src/replay.ts
878
+ var strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
879
+ function exposureKeyOf(v) {
880
+ const k = v;
881
+ if (!k || typeof k !== "object") return null;
882
+ const { kid, n, e, pkcs1 } = k;
883
+ return typeof kid === "string" && typeof n === "string" && typeof e === "string" && typeof pkcs1 === "string" ? { kid, n, e, pkcs1 } : null;
884
+ }
885
+ var fingerprint = (events) => sha256Hex2(JSON.stringify(events.find((e) => e.type === 2)?.data ?? null));
886
+ var screenAllowed = (screen, allow = [], deny = []) => !deny.includes(screen) && (allow.length === 0 || allow.includes(screen));
887
+ var effectiveSampleRate = (local, remote) => remote === null ? local : remote.session_recording === false ? 0 : Math.min(local, remote.replay_sample_rate ?? 1);
888
+ var effectiveMinDurationMs = (local, remote) => Math.max(local, remote?.replay_min_duration_ms ?? 0);
889
+ function bodyId(events) {
890
+ const find = (n) => {
891
+ const o = n;
892
+ if (!o) return void 0;
893
+ if (o.tagName === "body") return o.id;
894
+ for (const c of o.childNodes ?? []) {
895
+ const r = find(c);
896
+ if (r !== void 0) return r;
897
+ }
898
+ return void 0;
899
+ };
900
+ return find(events.find((e) => e.type === 2)?.data.node) ?? 0;
901
+ }
902
+ function createReplay(d) {
903
+ const { core, config } = d;
904
+ const now = d.now ?? Date.now;
905
+ let screen = "Unknown";
906
+ let timer;
907
+ let lastFp = null;
908
+ let lastScreen = null;
909
+ let lastSession = null;
910
+ let seq = 0;
911
+ let paused = false;
912
+ let lastCaptureAt = Number.NEGATIVE_INFINITY;
913
+ let remote = null;
914
+ let remoteLoaded = false;
915
+ let sessionStart = 0;
916
+ let held = [];
917
+ const maxHeld = 20;
918
+ let touches = [];
919
+ let tapTarget = 0;
920
+ let tapTree;
921
+ let hasSnapshot = false;
922
+ let patternSources = config.maskTextPatterns ?? [];
923
+ let patterns = compilePatterns(patternSources);
924
+ const sensitive = createSensitiveCapture(config.module);
925
+ let swipeStart = null;
926
+ let lastDeadSwipeAt;
927
+ const sampled = /* @__PURE__ */ new Map();
928
+ const isSampled = (sid) => {
929
+ const rate = effectiveSampleRate(config.sampleRate ?? 1, remote);
930
+ if (rate >= 1) return true;
931
+ if (rate <= 0) return false;
932
+ let v = sampled.get(sid);
933
+ if (v === void 0) {
934
+ v = (config.random ?? Math.random)() < rate;
935
+ sampled.clear();
936
+ sampled.set(sid, v);
937
+ }
938
+ return v;
939
+ };
940
+ const gateOpen = () => {
941
+ if (config.linkedFlag && !(d.isFlagOn?.(config.linkedFlag) ?? false)) return false;
942
+ return screenAllowed(screen, config.screenAllowList, config.screenDenyList);
943
+ };
944
+ const consented = () => core.hasConsent("analytics") && core.hasConsent("replay");
945
+ async function loadRemote() {
946
+ if (config.useRemoteConfig === false || remoteLoaded) return;
947
+ remoteLoaded = true;
948
+ try {
949
+ const r = await d.fetch(`${d.apiHost}/v1/config?api_key=${encodeURIComponent(d.apiKey)}`);
950
+ if (r.ok) remote = await r.json();
951
+ } catch {
952
+ }
953
+ sampled.clear();
954
+ const fromRemote = strings(remote?.replay_mask_text_patterns).filter((p) => p.length <= 200).slice(0, 20);
955
+ patternSources = [...config.maskTextPatterns ?? [], ...fromRemote];
956
+ patterns = compilePatterns(patternSources);
957
+ }
958
+ const isScreenMasked = () => screenMasked(screen, [...config.maskScreens ?? [], ...strings(remote?.replay_mask_screens)]);
959
+ const touchesRecording = () => hasSnapshot && !paused && consented() && gateOpen();
960
+ async function structure() {
961
+ try {
962
+ const raw = await config.module.captureWireframe({ screen, maskText: true, maskAllTextInputs: true, maskAllImages: true, maskSandboxedViews: true });
963
+ const events = raw ? JSON.parse(raw).events ?? [] : [];
964
+ return events.some((e) => e.type === 2) ? fingerprint(events) : null;
965
+ } catch {
966
+ return null;
967
+ }
968
+ }
969
+ async function swipeEnded(dx, dy) {
970
+ const pending = swipeStart;
971
+ swipeStart = null;
972
+ if (!pending || Math.hypot(dx, dy) < DEAD_SWIPE_MIN_DISTANCE) return false;
973
+ const start = await pending;
974
+ if (!start) return false;
975
+ await new Promise((r) => setTimeout(r, config.deadSwipeSettleMs ?? DEAD_SWIPE_SETTLE_MS));
976
+ if (!touchesRecording()) return false;
977
+ const fp = await structure();
978
+ if (fp === null) return false;
979
+ const t = now();
980
+ const p = deadSwipeDecision({ screen, startScreen: start.screen, startFingerprint: start.fp, endFingerprint: fp, dx, dy, nowMs: t, lastAt: lastDeadSwipeAt });
981
+ if (!p) return false;
982
+ lastDeadSwipeAt = t;
983
+ core.capture("$dead_swipe", p);
984
+ return true;
985
+ }
986
+ function captureSensitive(field, value) {
987
+ const key = exposureKeyOf(remote?.replay_exposure_key);
988
+ if (!value || !key || !strings(remote?.replay_exposure_fields).includes(field) || !consented()) return false;
989
+ const p = sensitive.properties(field, value, screen, key, core.session.touch().sessionId);
990
+ if (!p) return false;
991
+ core.capture("$sensitive_capture", p);
992
+ return true;
993
+ }
994
+ async function send(events, sessionId, windowId) {
995
+ const body = JSON.stringify({ api_key: d.apiKey, session_id: sessionId, window_id: windowId, seq, events });
996
+ if (body.length > (config.maxEventBytes ?? 5e6)) return;
997
+ try {
998
+ const r = await d.fetch(`${d.apiHost}/v1/replay`, { method: "POST", headers: { "Content-Type": "application/json" }, body });
999
+ if (r.ok) seq += 1;
1000
+ else lastFp = null;
1001
+ } catch {
1002
+ lastFp = null;
1003
+ }
1004
+ }
1005
+ async function tick() {
1006
+ if (!consented()) {
1007
+ lastFp = null;
1008
+ hasSnapshot = false;
1009
+ touches = [];
1010
+ held = [];
1011
+ return;
1012
+ }
1013
+ await loadRemote();
1014
+ if (paused || !gateOpen()) {
1015
+ lastFp = null;
1016
+ hasSnapshot = false;
1017
+ touches = [];
1018
+ return;
1019
+ }
1020
+ if (!isSampled(core.session.touch().sessionId)) return;
1021
+ const shots = config.mode === "screenshot";
1022
+ if (now() - lastCaptureAt < (shots ? Math.max(config.throttleMs ?? 0, 1e3) : config.throttleMs ?? 0)) return;
1023
+ lastCaptureAt = now();
1024
+ let cap;
1025
+ try {
1026
+ const masked = isScreenMasked();
1027
+ const raw = await config.module.captureWireframe({
1028
+ screen,
1029
+ maskText: masked || (config.maskText ?? true),
1030
+ maskAllTextInputs: masked || (config.maskAllTextInputs ?? true),
1031
+ maskAllImages: masked || (config.maskAllImages ?? true),
1032
+ maskSandboxedViews: config.maskSandboxedViews ?? true,
1033
+ ...masked ? { maskScreen: true } : {},
1034
+ ...patternSources.length > 0 ? { maskTextPatterns: patternSources } : {},
1035
+ ...shots ? { mode: "screenshot", screenshotQuality: Math.min(1, Math.max(0, config.screenshotQuality ?? 0.4)) } : {}
1036
+ });
1037
+ if (!raw) return;
1038
+ cap = JSON.parse(raw);
1039
+ } catch {
1040
+ return;
1041
+ }
1042
+ const events = (cap.events ?? []).map(
1043
+ (e) => e.type === 2 && !shots && patterns.length > 0 ? { ...e, data: { ...e.data, node: maskSnapshotText(e.data.node, patterns) } } : e
1044
+ );
1045
+ if (!events.some((e) => e.type === 2)) return;
1046
+ const { sessionId, windowId } = core.session.touch();
1047
+ const rotated = sessionId !== lastSession;
1048
+ if (rotated) {
1049
+ seq = 0;
1050
+ lastSession = sessionId;
1051
+ sessionStart = now();
1052
+ held = [];
1053
+ }
1054
+ const fp = fingerprint(events);
1055
+ const ts = now();
1056
+ let out = touches.splice(0);
1057
+ if (rotated || fp !== lastFp) {
1058
+ lastFp = fp;
1059
+ const snap = events.map((e) => ({ ...e, timestamp: ts }));
1060
+ if (screen !== lastScreen) {
1061
+ lastScreen = screen;
1062
+ snap.splice(1, 0, { type: 5, timestamp: ts, data: { tag: "navigation", payload: { url: `app://${screen}` } } });
1063
+ }
1064
+ out = [...out, ...snap];
1065
+ tapTarget = bodyId(events);
1066
+ tapTree = events.find((e) => e.type === 2)?.data.node;
1067
+ hasSnapshot = true;
1068
+ }
1069
+ if (out.length === 0) return;
1070
+ const minMs = effectiveMinDurationMs(config.minDurationMs ?? 0, remote);
1071
+ if (minMs > 0 && now() - sessionStart < minMs) {
1072
+ held.push(out);
1073
+ if (held.length > maxHeld) held.shift();
1074
+ return;
1075
+ }
1076
+ const backlog = held;
1077
+ held = [];
1078
+ for (const chunk of [...backlog, out]) await send(chunk, sessionId, windowId);
1079
+ }
1080
+ return {
1081
+ start() {
1082
+ if (timer !== void 0) return;
1083
+ timer = (config.setInterval ?? ((fn, ms) => setInterval(fn, ms)))(() => void tick(), config.snapshotIntervalMs ?? 5e3);
1084
+ },
1085
+ stop() {
1086
+ if (timer === void 0) return;
1087
+ (config.clearInterval ?? ((h) => clearInterval(h)))(timer);
1088
+ timer = void 0;
1089
+ held = [];
1090
+ },
1091
+ pause() {
1092
+ paused = true;
1093
+ },
1094
+ resume() {
1095
+ paused = false;
1096
+ },
1097
+ isRecording: () => timer !== void 0 && !paused && consented() && isSampled(core.session.touch().sessionId) && gateOpen(),
1098
+ setScreen(name) {
1099
+ screen = name;
1100
+ },
1101
+ tap(x, y) {
1102
+ if (config.captureTouches === false || !hasSnapshot || paused || !consented() || !gateOpen() || touches.length >= 500) return;
1103
+ touches.push({
1104
+ type: 3,
1105
+ timestamp: now(),
1106
+ data: { source: 2, type: 2, id: hitTest(tapTree, x, y, tapTarget), x, y }
1107
+ });
1108
+ },
1109
+ tick,
1110
+ swipeBegan() {
1111
+ if (config.captureDeadSwipes === false || !touchesRecording()) return;
1112
+ const s = screen;
1113
+ swipeStart = structure().then((fp) => fp === null ? null : { screen: s, fp });
1114
+ },
1115
+ swipeEnded,
1116
+ captureSensitive
1117
+ };
1118
+ }
1119
+
1120
+ // src/client.ts
1121
+ var LIB = "lucida-react-native";
1122
+ var LIB_VERSION = "0.0.0";
1123
+ var DEFAULT_HOST = "https://app.lucida.dev";
1124
+ var VERSION_KEY = "lc_app_version";
1125
+ var noop = { send: async () => ({ status: 200 }) };
1126
+ async function createClient(apiKey, config = {}) {
1127
+ const apiHost = (config.apiHost ?? DEFAULT_HOST).replace(/\/+$/, "");
1128
+ const boot = config.flagOptions?.bootstrap;
1129
+ const fetchImpl = config.fetch ?? ((...a) => fetch(...a));
1130
+ let debugOn = !!config.debug;
1131
+ const log = (...a) => {
1132
+ if (debugOn) console.debug("[lucida]", ...a);
1133
+ };
1134
+ let storage = memoryStorage();
1135
+ if (config.asyncStorage && !config.disabled) {
1136
+ const a = asyncStorageAdapter(config.asyncStorage);
1137
+ await a.ready;
1138
+ storage = a.storage;
1139
+ }
1140
+ if (config.queueCipher) storage = encryptedStorage(storage, config.queueCipher, ["lc_offline"]);
1141
+ let transport = config.transport ?? fetchTransport(fetchImpl, config.requestTimeoutMs);
1142
+ if (config.maxBatchSize) transport = splitBatches(transport, config.maxBatchSize);
1143
+ if (config.transport && config.requestTimeoutMs) transport = withTimeout(transport, config.requestTimeoutMs);
1144
+ let offline;
1145
+ if (!config.transport && config.asyncStorage) transport = offline = offlineTransport(transport, storage);
1146
+ let flagsRef;
1147
+ const core = new Core({
1148
+ apiKey,
1149
+ apiHost,
1150
+ transport: config.disabled ? noop : transport,
1151
+ storage,
1152
+ library: { name: LIB, version: LIB_VERSION },
1153
+ ...config.consent ? { consent: config.consent } : {},
1154
+ ...boot?.distinctId ? { bootstrapDistinctId: boot.distinctId } : {},
1155
+ session: {
1156
+ ...config.sessionIdleMs ? { idleMs: config.sessionIdleMs } : {},
1157
+ ...config.sessionMaxMs ? { maxMs: config.sessionMaxMs } : {}
1158
+ },
1159
+ queue: {
1160
+ ...config.flushAt ? { flushAt: config.flushAt } : {},
1161
+ ...config.flushIntervalMs ? { flushIntervalMs: config.flushIntervalMs } : {},
1162
+ ...config.maxQueueSize ? { maxQueueSize: config.maxQueueSize } : {},
1163
+ ...config.maxRetries !== void 0 ? { maxRetries: config.maxRetries } : {},
1164
+ ...config.retryDelayMs ? { backoff: { baseMs: config.retryDelayMs } } : {}
1165
+ },
1166
+ enrich: (event) => ({
1167
+ properties: {
1168
+ $os: config.platform,
1169
+ $os_version: config.osVersion,
1170
+ $app_version: config.appVersion,
1171
+ $app_build: config.appBuild,
1172
+ ...config.sendFeatureFlags !== false && event !== "$snapshot" && flagsRef ? flagEventProps(flagsRef.all()) : {}
1173
+ }
1174
+ }),
1175
+ log
1176
+ });
1177
+ if (boot?.isIdentified && boot.distinctId === core.distinctId) core.identity.identify(boot.distinctId);
1178
+ const person = installPolicy(core, {
1179
+ beforeSend: config.beforeSend,
1180
+ personProfiles: config.personProfiles,
1181
+ disableGeoip: config.disableGeoip,
1182
+ log
1183
+ });
1184
+ if (config.dataMode === "wifi" && config.network) {
1185
+ const q = core.queue;
1186
+ const send = q.flush.bind(q);
1187
+ q.flush = (o) => {
1188
+ if (config.network.isWifi()) return send(o);
1189
+ setTimeout(() => void q.flush(), config.flushIntervalMs ?? 5e3);
1190
+ return Promise.resolve();
1191
+ };
1192
+ }
1193
+ const whenConsented = (fn) => {
1194
+ if (config.disabled) return;
1195
+ if (core.hasConsent("analytics")) return fn();
1196
+ let done = false;
1197
+ core.onChange((c) => {
1198
+ if (!done && c === "consent" && core.hasConsent("analytics")) {
1199
+ done = true;
1200
+ fn();
1201
+ }
1202
+ });
1203
+ };
1204
+ const deps = { core, fetch: fetchImpl, apiHost, apiKey, whenConsented };
1205
+ const flags = createFlags({
1206
+ ...deps,
1207
+ localFlags: config.localFlags,
1208
+ localFlagsContext: config.localFlagsContext,
1209
+ flagStream: config.flagStream,
1210
+ appState: config.appState,
1211
+ options: config.flagOptions,
1212
+ disableRemote: config.disableRemoteFeatureFlags,
1213
+ ...config.sendDefaultFlagProps === false ? {} : { defaultProps: { $lib: LIB, ...config.appVersion ? { $app_version: config.appVersion } : {}, ...config.platform ? { $os: config.platform } : {} } }
1214
+ });
1215
+ flagsRef = flags;
1216
+ const autocapture = createAutocapture(core, config.autocapture ?? {});
1217
+ const headersFor = tracingHeaders(config.tracingHeaders ?? [], apiHost, () => core.hasConsent("analytics"), () => ({
1218
+ distinctId: core.distinctId,
1219
+ sessionId: core.sessionId
1220
+ }));
1221
+ const anonId = () => {
1222
+ try {
1223
+ return JSON.parse(core.storage.get("lc_identity") ?? "{}").anon_id ?? core.distinctId;
1224
+ } catch {
1225
+ return core.distinctId;
1226
+ }
1227
+ };
1228
+ const personBlocked = (what) => {
1229
+ if (person.blocksPersonWrites) log(`personProfiles=never: ${what} ignored`);
1230
+ return person.blocksPersonWrites;
1231
+ };
1232
+ const guides = createGuides({ core, fetch: fetchImpl, apiHost, apiKey, flag: (k) => ![void 0, false].includes(flags.get(k)) });
1233
+ if (config.guides?.enabled && !config.disabled) {
1234
+ const load = () => void flags.reload().finally(() => void guides.reload());
1235
+ whenConsented(load);
1236
+ core.onChange((c) => {
1237
+ if (c === "identify" || c === "reset" || c === "group") load();
1238
+ });
1239
+ }
1240
+ const replay = config.replay?.enabled && !config.disabled ? createReplay({ core, fetch: fetchImpl, apiHost, apiKey, config: config.replay, isFlagOn: (k) => {
1241
+ const v = flags.all()[k];
1242
+ return v !== void 0 && v !== false;
1243
+ } }) : void 0;
1244
+ const interventions = interventionHub(core, { apiHost, apiKey, fetch: fetchImpl, whenConsented, log, disabled: !!config.disabled });
1245
+ const client = {
1246
+ get distinctId() {
1247
+ return core.distinctId;
1248
+ },
1249
+ getAnonymousId: anonId,
1250
+ isOptedOut: () => !core.hasConsent("analytics"),
1251
+ debug(on) {
1252
+ debugOn = on;
1253
+ },
1254
+ capture: (e, p, o) => void core.capture(e, p, { ...o?.set ? { $set: o.set } : {}, ...o?.setOnce ? { $set_once: o.setOnce } : {} }),
1255
+ setPersonProperties(set, setOnce) {
1256
+ if (personBlocked("setPersonProperties")) return;
1257
+ core.capture("$set", {}, { ...set ? { $set: set } : {}, ...setOnce ? { $set_once: setOnce } : {} });
1258
+ },
1259
+ openURL(url, referrer) {
1260
+ const query = {};
1261
+ for (const kv of (url.split("#")[0].split("?")[1] ?? "").split("&")) {
1262
+ if (!kv) continue;
1263
+ const [k = "", v = ""] = kv.split("=");
1264
+ query[decodeURIComponent(k)] = decodeURIComponent(v);
1265
+ }
1266
+ core.capture("$deep_link_opened", { url, query, ...referrer ? { referrer } : {} });
1267
+ },
1268
+ fetch: tracedFetch(fetchImpl, headersFor),
1269
+ autocapture,
1270
+ screen(name, props) {
1271
+ core.capture("$screen", { ...props, $screen_name: name });
1272
+ replay?.setScreen(name);
1273
+ if (config.guides?.enabled) guides.show(name);
1274
+ },
1275
+ identify(id, s, o) {
1276
+ if (!personBlocked("identify")) core.identify(id, s, o);
1277
+ },
1278
+ alias(a, o) {
1279
+ if (!personBlocked("alias")) core.alias(a, o);
1280
+ },
1281
+ reset() {
1282
+ person.reset();
1283
+ core.reset({ reuseAnonymousId: !!config.reuseAnonymousId });
1284
+ },
1285
+ register: (p) => core.register(p),
1286
+ registerOnce: (p) => core.registerOnce(p),
1287
+ unregister: (k) => core.unregister(k),
1288
+ group: (t, k, p) => core.group(t, k, p),
1289
+ optIn: () => core.optIn(),
1290
+ optOut: () => core.optOut(),
1291
+ getFlag: (k) => flags.get(k),
1292
+ getFlagPayload: (k) => flags.payload(k),
1293
+ getFlagResult: (k, o) => flags.result(k, o),
1294
+ isFeatureEnabled: (k) => {
1295
+ const v = flags.get(k);
1296
+ return v !== void 0 && v !== false;
1297
+ },
1298
+ onFlags: (cb) => flags.onChange(cb),
1299
+ onIntervention: (cb) => interventions.on(cb),
1300
+ reloadFlags: () => flags.reload(),
1301
+ updateFlags: (f, p, o) => flags.update(f, p, o),
1302
+ getAllFlags: () => flags.all(),
1303
+ overrideFlags: (m) => flags.override(m),
1304
+ setPersonPropertiesForFlags: (p, r) => flags.setPersonProperties(p, r),
1305
+ resetPersonPropertiesForFlags: (r) => flags.resetPersonProperties(r),
1306
+ setGroupPropertiesForFlags: (t, p, r) => flags.setGroupProperties(t, p, r),
1307
+ resetGroupPropertiesForFlags: (r) => flags.resetGroupProperties(r),
1308
+ replay: {
1309
+ start: () => replay?.start(),
1310
+ stop: () => replay?.stop(),
1311
+ pause: () => replay?.pause(),
1312
+ resume: () => replay?.resume(),
1313
+ tag: (name) => void core.capture("$replay_tag", { name, session_id: core.sessionId }),
1314
+ isRecording: () => replay?.isRecording() ?? false,
1315
+ tap: (x, y) => replay?.tap(x, y),
1316
+ swipeBegan: () => replay?.swipeBegan(),
1317
+ swipeEnded: (dx, dy) => void replay?.swipeEnded(dx, dy)
1318
+ },
1319
+ captureSensitive: (field, value) => replay?.captureSensitive(field, value) ?? false,
1320
+ guides,
1321
+ flush: () => core.flush(),
1322
+ shutdown: async () => {
1323
+ stopLifecycle();
1324
+ flags.stop();
1325
+ interventions.stop();
1326
+ replay?.stop();
1327
+ await core.shutdown();
1328
+ }
1329
+ };
1330
+ const opened = () => void core.capture("$app_opened");
1331
+ const prev = core.storage.get(VERSION_KEY);
1332
+ if (config.appVersion && prev !== config.appVersion) {
1333
+ const base = { $app_version: config.appVersion };
1334
+ if (prev !== null) core.capture("$app_updated", { ...base, $previous_app_version: prev });
1335
+ else if (config.installInfo && config.installAttribution !== false) {
1336
+ void resolveInstallProps(config.installInfo, { fetch: fetchImpl }).then((extra) => core.capture("$app_installed", { ...base, ...extra }));
1337
+ } else core.capture("$app_installed", base);
1338
+ core.storage.set(VERSION_KEY, config.appVersion);
1339
+ }
1340
+ opened();
1341
+ const stopLifecycle = config.appState ? appLifecycle(config.appState, {
1342
+ onForeground() {
1343
+ opened();
1344
+ void offline?.drain();
1345
+ },
1346
+ onBackground() {
1347
+ core.capture("$app_backgrounded");
1348
+ void core.flush();
1349
+ }
1350
+ }) : () => void 0;
1351
+ replay?.start();
1352
+ if (offline) whenConsented(() => void offline.drain());
1353
+ log("initialised", { apiHost });
1354
+ return client;
1355
+ }
1356
+
1357
+ // src/native-cipher.ts
1358
+ function nativeQueueCipher(module) {
1359
+ if (!module || typeof module.encryptQueue !== "function" || typeof module.decryptQueue !== "function") {
1360
+ throw new Error("nativeQueueCipher: the native LucidaReplay module has no encryptQueue/decryptQueue (rebuild the app, and note blocking sync methods do not work under the remote JS debugger)");
1361
+ }
1362
+ return {
1363
+ encrypt(plain) {
1364
+ try {
1365
+ const out = module.encryptQueue(plain);
1366
+ return typeof out === "string" ? out : "";
1367
+ } catch {
1368
+ return "";
1369
+ }
1370
+ },
1371
+ decrypt(cipher) {
1372
+ if (cipher === "") return null;
1373
+ try {
1374
+ const out = module.decryptQueue(cipher);
1375
+ return typeof out === "string" && out !== "" ? out : null;
1376
+ } catch {
1377
+ return null;
1378
+ }
1379
+ }
1380
+ };
1381
+ }
1382
+
1383
+ // src/navigation.ts
1384
+ function screenTracker(client, ref, opts = {}) {
1385
+ let last;
1386
+ const track = () => {
1387
+ const r = ref.current?.getCurrentRoute();
1388
+ if (!r || r.name === last) return;
1389
+ last = r.name;
1390
+ client.screen(r.name, opts.includeParams && r.params ? { $screen_params: r.params } : {});
1391
+ };
1392
+ return { onReady: track, onStateChange: track };
1393
+ }
1394
+
1395
+ // src/guides-view.tsx
1396
+ import { useContext as useContext2, useEffect as useEffect3, useState as useState2 } from "react";
1397
+ import { Dimensions, Linking, Modal, Pressable, Text, View as View2, useColorScheme } from "react-native";
1398
+
1399
+ // src/guide-anchor.tsx
1400
+ import { useEffect, useRef } from "react";
1401
+ import { View } from "react-native";
1402
+ import { jsx } from "react/jsx-runtime";
1403
+ var anchors = /* @__PURE__ */ new Map();
1404
+ function registerAnchor(id, m) {
1405
+ anchors.set(id, m);
1406
+ return () => {
1407
+ if (anchors.get(id) === m) anchors.delete(id);
1408
+ };
1409
+ }
1410
+ function measureAnchor(id) {
1411
+ const m = anchors.get(id);
1412
+ if (!m) return Promise.resolve(null);
1413
+ return new Promise((resolve) => {
1414
+ try {
1415
+ m.measureInWindow((x, y, width, height) => resolve(width > 0 || height > 0 ? { x, y, width, height } : null));
1416
+ } catch {
1417
+ resolve(null);
1418
+ }
1419
+ });
1420
+ }
1421
+ function GuideAnchor({ testID, children, style }) {
1422
+ const ref = useRef(null);
1423
+ useEffect(() => ref.current ? registerAnchor(testID, ref.current) : void 0, [testID]);
1424
+ return /* @__PURE__ */ jsx(View, { ref, testID, collapsable: false, style, children });
1425
+ }
1426
+
1427
+ // src/guide-theme.ts
1428
+ var HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
1429
+ var obj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
1430
+ function wantsDark(a, systemDark) {
1431
+ if (!obj(obj(a?.["dark"])?.["tokens"])) return false;
1432
+ const m = a?.["theme_mode"];
1433
+ const mode = typeof m === "string" ? m : "auto";
1434
+ return mode === "dark" || mode === "auto" && systemDark;
1435
+ }
1436
+ function guideTokens(a, systemDark) {
1437
+ const t = {};
1438
+ const ff = a?.["font_family"];
1439
+ if (typeof ff === "string") t.font = ff;
1440
+ const layers = [obj(a?.["tokens"]), wantsDark(a, systemDark) ? obj(obj(a?.["dark"])?.["tokens"]) : void 0];
1441
+ for (const l of layers) {
1442
+ if (!l) continue;
1443
+ for (const k of ["background", "text", "accent"]) {
1444
+ const v = l[k];
1445
+ if (typeof v === "string" && HEX.test(v)) t[k] = v;
1446
+ }
1447
+ const r = l["radius"];
1448
+ if (typeof r === "number" && r >= 0 && r <= 32) t.radius = r;
1449
+ const f = l["font"];
1450
+ if (typeof f === "string" && f.trim()) t.font = f;
1451
+ }
1452
+ return t;
1453
+ }
1454
+ function bubbleOrigin(w, h, r, bounds, placement, margin = 12, gap = 8) {
1455
+ const below = r.y + r.height + gap;
1456
+ const above = r.y - gap - h;
1457
+ const fitsBelow = below + h <= bounds.y + bounds.height - margin;
1458
+ const fitsAbove = above >= bounds.y + margin;
1459
+ const y = placement === "top" ? fitsAbove || !fitsBelow ? above : below : fitsBelow || !fitsAbove ? below : above;
1460
+ const clamp = (v, lo, hi) => Math.min(Math.max(v, lo), Math.max(lo, hi));
1461
+ return {
1462
+ x: clamp(r.x + r.width / 2 - w / 2, bounds.x + margin, bounds.x + bounds.width - margin - w),
1463
+ y: clamp(y, bounds.y + margin, bounds.y + bounds.height - margin - h)
1464
+ };
1465
+ }
1466
+
1467
+ // src/provider.tsx
1468
+ import { createContext, useContext, useEffect as useEffect2, useState } from "react";
1469
+ import { jsx as jsx2 } from "react/jsx-runtime";
1470
+ var LucidaContext = createContext(null);
1471
+ function LucidaProvider({ client: given, apiKey, options, children }) {
1472
+ const [client, setClient] = useState(given ?? null);
1473
+ const [flags, setFlags] = useState({});
1474
+ useEffect2(() => {
1475
+ if (given) return setClient(given);
1476
+ if (!apiKey) return;
1477
+ let alive = true;
1478
+ let owned;
1479
+ void createClient(apiKey, options).then((c) => {
1480
+ owned = c;
1481
+ if (alive) setClient(c);
1482
+ else void c.shutdown();
1483
+ });
1484
+ return () => {
1485
+ alive = false;
1486
+ void owned?.shutdown();
1487
+ };
1488
+ }, [given, apiKey]);
1489
+ useEffect2(() => client?.onFlags((f) => setFlags(f)), [client]);
1490
+ return /* @__PURE__ */ jsx2(LucidaContext.Provider, { value: { client, flags }, children });
1491
+ }
1492
+ function useLucidaContext() {
1493
+ const v = useContext(LucidaContext);
1494
+ if (!v) throw new Error("Lucida hooks must be used inside <LucidaProvider>");
1495
+ return v;
1496
+ }
1497
+
1498
+ // src/guides-view.tsx
1499
+ import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
1500
+ function LucidaGuides() {
1501
+ const client = useContext2(LucidaContext)?.client;
1502
+ const [v, setV] = useState2(null);
1503
+ const dark = useColorScheme() === "dark";
1504
+ useEffect3(() => {
1505
+ if (!client) return;
1506
+ client.guides.setOpener((u) => void Linking.openURL(u).catch(() => void 0));
1507
+ return client.guides.subscribe(setV);
1508
+ }, [client]);
1509
+ if (!client || !v) return null;
1510
+ const t = guideTokens(v.guide.appearance, dark);
1511
+ const act = (b) => client.guides.act(b);
1512
+ if (v.anchor) return /* @__PURE__ */ jsx3(AnchoredStep, { v, t, act, missing: () => client.guides.targetMissing() }, `${v.guide.id}:${v.step}`);
1513
+ const step = v.guide.steps[v.step];
1514
+ const banner = v.guide.kind === "banner" || v.guide.kind === "announcement";
1515
+ return /* @__PURE__ */ jsx3(Modal, { visible: true, transparent: true, animationType: "fade", onRequestClose: () => act(null), children: /* @__PURE__ */ jsx3(View2, { style: { flex: 1, justifyContent: banner ? "flex-start" : "center", backgroundColor: "rgba(0,0,0,0.4)", padding: 16 }, children: /* @__PURE__ */ jsxs(View2, { accessibilityViewIsModal: true, style: { backgroundColor: t.background ?? "#fff", borderRadius: t.radius ?? 12, padding: 16 }, children: [
1516
+ /* @__PURE__ */ jsx3(Body, { v, t, title: step?.title ?? null }),
1517
+ v.guide.kind === "carousel" ? /* @__PURE__ */ jsx3(Dots, { i: v.step, n: v.guide.steps.length, t }) : null,
1518
+ /* @__PURE__ */ jsx3(Buttons, { v, t, act })
1519
+ ] }) }) });
1520
+ }
1521
+ function Body({ v, t, title }) {
1522
+ const step = v.guide.steps[v.step];
1523
+ const font = t.font?.split(",")[0]?.trim().replace(/["']/g, "");
1524
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1525
+ title ? /* @__PURE__ */ jsx3(Text, { accessibilityRole: "header", style: { fontSize: 18, fontWeight: "600", color: t.text, fontFamily: font }, children: title }) : null,
1526
+ step?.body ? /* @__PURE__ */ jsx3(Text, { style: { marginTop: 8, color: t.text, fontFamily: font }, children: step.body }) : null
1527
+ ] });
1528
+ }
1529
+ function Buttons({ v, t, act }) {
1530
+ return /* @__PURE__ */ jsx3(View2, { style: { flexDirection: "row", justifyContent: "flex-end", marginTop: 16 }, children: v.buttons.map((b, i) => /* @__PURE__ */ jsx3(Pressable, { accessibilityRole: "button", accessibilityLabel: b.label, onPress: () => act(b), style: { padding: 8, marginLeft: 8 }, children: /* @__PURE__ */ jsx3(Text, { style: { color: t.accent }, children: b.label }) }, i)) });
1531
+ }
1532
+ function Dots({ i, n, t }) {
1533
+ return /* @__PURE__ */ jsx3(View2, { accessibilityLabel: `Slide ${i + 1} of ${n}`, style: { flexDirection: "row", justifyContent: "center", marginTop: 12 }, children: Array.from({ length: n }, (_, k) => /* @__PURE__ */ jsx3(View2, { style: { width: 8, height: 8, borderRadius: 4, marginHorizontal: 3, backgroundColor: k === i ? t.accent ?? "#111" : "#ccc" } }, k)) });
1534
+ }
1535
+ function AnchoredStep({ v, t, act, missing }) {
1536
+ const a = v.anchor;
1537
+ const [rect, setRect] = useState2(null);
1538
+ const [open, setOpen] = useState2(!a.hotspot);
1539
+ const [height, setHeight] = useState2(120);
1540
+ useEffect3(() => {
1541
+ let alive = true;
1542
+ let found = false;
1543
+ let goneSince = 0;
1544
+ const deadline = Date.now() + a.waitMs;
1545
+ const poll = async () => {
1546
+ if (!alive) return;
1547
+ const r = await measureAnchor(a.nativeId);
1548
+ if (!alive) return;
1549
+ if (r) {
1550
+ found = true;
1551
+ goneSince = 0;
1552
+ setRect((p) => p && p.x === r.x && p.y === r.y && p.width === r.width && p.height === r.height ? p : r);
1553
+ } else if (!found && Date.now() >= deadline) return missing();
1554
+ else if (found) {
1555
+ goneSince ||= Date.now();
1556
+ if (Date.now() - goneSince > 2e3) return act(null);
1557
+ }
1558
+ setTimeout(() => void poll(), found ? 100 : 150);
1559
+ };
1560
+ void poll();
1561
+ return () => void (alive = false);
1562
+ }, [a.nativeId, a.waitMs]);
1563
+ if (!rect) return null;
1564
+ const win = Dimensions.get("window");
1565
+ const width = Math.min(300, win.width - 24);
1566
+ const at = bubbleOrigin(width, height, rect, { x: 0, y: 0, width: win.width, height: win.height }, a.placement);
1567
+ const step = v.guide.steps[v.step];
1568
+ return /* @__PURE__ */ jsxs(View2, { pointerEvents: "box-none", style: { position: "absolute", left: 0, top: 0, right: 0, bottom: 0 }, children: [
1569
+ a.hotspot ? /* @__PURE__ */ jsx3(
1570
+ Pressable,
1571
+ {
1572
+ accessibilityRole: "button",
1573
+ accessibilityLabel: step?.title ?? v.guide.name,
1574
+ onPress: () => setOpen(true),
1575
+ style: { position: "absolute", left: rect.x + rect.width - 8, top: rect.y - 8, width: 16, height: 16, borderRadius: 8, borderWidth: 2, borderColor: "#fff", backgroundColor: t.accent ?? "#e5484d" }
1576
+ }
1577
+ ) : null,
1578
+ open ? /* @__PURE__ */ jsxs(
1579
+ View2,
1580
+ {
1581
+ onLayout: (e) => setHeight(e.nativeEvent.layout.height),
1582
+ style: { position: "absolute", left: at.x, top: at.y, width, backgroundColor: t.background ?? "#fff", borderRadius: t.radius ?? 10, padding: 14, elevation: 6, shadowOpacity: 0.25, shadowRadius: 8 },
1583
+ children: [
1584
+ /* @__PURE__ */ jsx3(Body, { v, t, title: step?.title ?? (v.step === 0 ? v.guide.name : null) }),
1585
+ /* @__PURE__ */ jsx3(Buttons, { v, t, act })
1586
+ ]
1587
+ }
1588
+ ) : null
1589
+ ] });
1590
+ }
1591
+
1592
+ // src/mask.tsx
1593
+ import { useContext as useContext3, useRef as useRef2 } from "react";
1594
+ import { Platform, View as View3 } from "react-native";
1595
+ import { jsx as jsx4 } from "react/jsx-runtime";
1596
+ var REPLAY_MARKERS = {
1597
+ ios: { mask: "lucida-sensitive", unmask: "lucida-unmask" },
1598
+ android: { mask: "lc-mask", unmask: "lc-unmask" }
1599
+ };
1600
+ var replayMarker = (kind, os = Platform.OS) => REPLAY_MARKERS[os === "android" ? "android" : "ios"][kind];
1601
+ function LucidaMask({ children, style }) {
1602
+ return /* @__PURE__ */ jsx4(View3, { testID: replayMarker("mask"), collapsable: false, style, children });
1603
+ }
1604
+ function LucidaUnmask({ children, style }) {
1605
+ return /* @__PURE__ */ jsx4(View3, { testID: replayMarker("unmask"), collapsable: false, style, children });
1606
+ }
1607
+ var point = (e) => {
1608
+ const n = e.nativeEvent;
1609
+ return n && typeof n.pageX === "number" && typeof n.pageY === "number" ? { x: n.pageX, y: n.pageY } : null;
1610
+ };
1611
+ var TOUCH_SLOP = 10;
1612
+ function LucidaTouchCapture({ children, style }) {
1613
+ const client = useContext3(LucidaContext)?.client;
1614
+ const swipe = useRef2(null);
1615
+ return /* @__PURE__ */ jsx4(
1616
+ View3,
1617
+ {
1618
+ collapsable: false,
1619
+ style: style ?? { flex: 1 },
1620
+ onTouchStart: (e) => {
1621
+ const p = point(e);
1622
+ swipe.current = p ? { ...p, began: false } : null;
1623
+ },
1624
+ onTouchMove: (e) => {
1625
+ const s = swipe.current;
1626
+ const p = point(e);
1627
+ if (!client || !s || !p || s.began || Math.hypot(p.x - s.x, p.y - s.y) <= TOUCH_SLOP) return;
1628
+ s.began = true;
1629
+ client.replay.swipeBegan();
1630
+ },
1631
+ onTouchEnd: (e) => {
1632
+ const p = point(e);
1633
+ const s = swipe.current;
1634
+ swipe.current = null;
1635
+ if (!client || !p) return;
1636
+ client.replay.tap(p.x, p.y);
1637
+ if (s?.began) client.replay.swipeEnded(p.x - s.x, p.y - s.y);
1638
+ },
1639
+ children
1640
+ }
1641
+ );
1642
+ }
1643
+
1644
+ // src/hooks.tsx
1645
+ import { useCallback, useEffect as useEffect4, useMemo } from "react";
1646
+ import { Fragment as Fragment2, jsx as jsx5 } from "react/jsx-runtime";
1647
+ function useLucida() {
1648
+ return useLucidaContext().client;
1649
+ }
1650
+ function useCapture() {
1651
+ const { client } = useLucidaContext();
1652
+ return useCallback((event, props) => client?.capture(event, props), [client]);
1653
+ }
1654
+ var NO_REPLAY = { start() {
1655
+ }, stop() {
1656
+ }, pause() {
1657
+ }, resume() {
1658
+ }, tag() {
1659
+ }, tap() {
1660
+ }, swipeBegan() {
1661
+ }, swipeEnded() {
1662
+ }, isRecording: () => false };
1663
+ function useReplay() {
1664
+ return useLucidaContext().client?.replay ?? NO_REPLAY;
1665
+ }
1666
+ function useScreen(name, props) {
1667
+ const { client } = useLucidaContext();
1668
+ useEffect4(() => {
1669
+ client?.screen(name, props);
1670
+ }, [client, name]);
1671
+ }
1672
+ function useFeatureFlag(key) {
1673
+ const { client, flags } = useLucidaContext();
1674
+ const value = flags[key];
1675
+ useEffect4(() => {
1676
+ if (value !== void 0) client?.getFlag(key);
1677
+ }, [client, key, value]);
1678
+ return value;
1679
+ }
1680
+ function useFeatureFlagVariant(key) {
1681
+ const v = useFeatureFlag(key);
1682
+ return typeof v === "string" ? v : void 0;
1683
+ }
1684
+ function useFeatureFlagPayload(key) {
1685
+ const { client, flags } = useLucidaContext();
1686
+ return useMemo(() => client?.getFlagPayload(key), [client, key, flags]);
1687
+ }
1688
+ function useActiveFeatureFlags() {
1689
+ const { flags } = useLucidaContext();
1690
+ return useMemo(() => Object.keys(flags).filter((k) => flags[k] !== false), [flags]);
1691
+ }
1692
+ function FeatureGate({ flag, variant, fallback = null, children }) {
1693
+ const v = useFeatureFlag(flag);
1694
+ const on = variant !== void 0 ? v === variant : v !== void 0 && v !== false;
1695
+ return /* @__PURE__ */ jsx5(Fragment2, { children: on ? children : fallback });
1696
+ }
1697
+ export {
1698
+ DEAD_SWIPE_MIN_DISTANCE,
1699
+ FeatureGate,
1700
+ GuideAnchor,
1701
+ LIB,
1702
+ LIB_VERSION,
1703
+ LucidaGuides,
1704
+ LucidaMask,
1705
+ LucidaProvider,
1706
+ LucidaTouchCapture,
1707
+ LucidaUnmask,
1708
+ PERSISTED_KEYS,
1709
+ PersonMode,
1710
+ REPLAY_MARKERS,
1711
+ SENSITIVE_MAX_PLAIN_BYTES,
1712
+ appLifecycle,
1713
+ appStoreInstallProps,
1714
+ asyncStorageAdapter,
1715
+ bubbleOrigin,
1716
+ chainFromFiber,
1717
+ compilePatterns,
1718
+ createAutocapture,
1719
+ createClient,
1720
+ createGuides,
1721
+ createReplay,
1722
+ createSensitiveCapture,
1723
+ deadSwipeDecision,
1724
+ effectiveMinDurationMs,
1725
+ effectiveSampleRate,
1726
+ encryptSensitive,
1727
+ encryptedStorage,
1728
+ exposureKeyOf,
1729
+ fetchTransport,
1730
+ guideAnchor,
1731
+ guideButtons,
1732
+ guideTokens,
1733
+ installReferrerProps,
1734
+ maskPatterns,
1735
+ maskSnapshotText,
1736
+ matches as matchGuideScreen,
1737
+ measureAnchor,
1738
+ memoryStorage,
1739
+ nativeQueueCipher,
1740
+ registerAnchor,
1741
+ replayMarker,
1742
+ resolveInstallProps,
1743
+ runHooks,
1744
+ sampleHook,
1745
+ screenAllowed,
1746
+ screenExcluded,
1747
+ screenMasked,
1748
+ screenTracker,
1749
+ splitBatches,
1750
+ swipeDirection,
1751
+ touchHandler,
1752
+ tracedFetch,
1753
+ tracingHeaders,
1754
+ truncateUtf8,
1755
+ useActiveFeatureFlags,
1756
+ useCapture,
1757
+ useFeatureFlag,
1758
+ useFeatureFlagPayload,
1759
+ useFeatureFlagVariant,
1760
+ useLucida,
1761
+ useReplay,
1762
+ useScreen,
1763
+ withBaggage,
1764
+ withTimeout
1765
+ };