@guidelytech/react-native 0.13.0 → 0.15.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 CHANGED
@@ -88,7 +88,7 @@ tasks).
88
88
  | `<GuidelyMask>` | Keeps its children out of tap-analytics text and session recordings (it sets its own `nativeID`) |
89
89
 
90
90
  Provider props: `projectId`, `apiUrl`, `environment`, `storage`, `user`, `debug`, `analytics`,
91
- `consent` (`granted` / `pending` / `denied`), `appVersion` (for *App version* rules), `theme`,
91
+ `consent` (`granted` / `pending` / `denied`), `appVersion` (overrides the detected version, see below), `theme`,
92
92
  `configRefreshMs`, `onNavigate`, `onError`, `showChecklistLauncher` (default `true`),
93
93
  `showResourceCenterLauncher` (default `false`), `showMessengerLauncher` (default `true`), `insets`,
94
94
  `launcherOffset` (default 72), `autocapture`, `appBuild`, `sessionReplay`.
@@ -111,7 +111,7 @@ runs when the project turns it on (Settings → Product analytics) and analytics
111
111
  | `rage_click` | 3+ taps within 1 s and 30 pt | the `click` properties of the tapped (or hit) view + `count` |
112
112
  | `heatmap` | Every 40 taps, on screen change and when the app goes to the background | `path?`, `viewportWidth`, `viewportHeight`, `pageHeight`, `platform`, `points` |
113
113
  | `page_leave` | The screen changes or the app goes to the background | `path?`, `screen`, `durationMs`, `scrollDepth: 100`, `platform` |
114
- | `page_view` | `screen()` with a new name; after returning to the foreground with `resumed: true` | `path`, `screen`, `platform` |
114
+ | `page_view` | `screen()` with a new name; after returning to the foreground with `resumed: true` | `path`, `screen`, `platform`, `deviceType`, `os`, `osVersion` |
115
115
  | `app_install` / `app_update` | First launch / a launch with a new `appVersion` or `appBuild` | `appVersion`, `build`, `previousVersion`, `previousBuild` |
116
116
  | `app_open` / `app_background` | Cold start and return to the foreground / background | `coldStart` / `foregroundMs` |
117
117
 
@@ -141,10 +141,18 @@ import { GuidelyMask } from "@guidelytech/react-native";
141
141
  <GuidelyMask><Pressable onPress={showCard}><Text>{cardNumber}</Text></Pressable></GuidelyMask>
142
142
  ```
143
143
 
144
- **App lifecycle.** `app_install` / `app_update` need the `appVersion` prop (e.g.
145
- `Application.nativeApplicationVersion` from `expo-application`; `appBuild` from
146
- `nativeBuildVersion`) and `storage`; the last version is stored per public key. An app that already
147
- had Guidely installed stores its version silently on the first launch with this release.
144
+ **App and OS versions.** The SDK detects them on iOS and Android. The OS version (`18.1`, or `14` on
145
+ Android) comes from React Native and is sent as `osVersion` on `page_view` and as the `osVersion`
146
+ flag attribute. React Native has no API for the app's own version and build, so they are read from
147
+ the first linked native module that knows them: `@guidelytech/react-native-replay`
148
+ (`CFBundleShortVersionString` / `versionName`, `CFBundleVersion` / `versionCode`), then
149
+ `expo-application` (not in Expo Go, which reports its own version), then `react-native-device-info`.
150
+ If none of these is installed, pass `appVersion` / `appBuild`. Passing them also overrides the
151
+ detected values.
152
+
153
+ **App lifecycle.** `app_install` / `app_update` need a known app version (detected or the
154
+ `appVersion` prop) and `storage`. The last version is stored per public key. An app that already had
155
+ Guidely installed stores its version silently on the first launch with this release.
148
156
 
149
157
  **Limitations.** React Native has no public API for "which component is under this point", so
150
158
  element resolution (steps 2 and 3, `tag`, `text`) reads React's internal component tree of the touch
@@ -217,7 +225,7 @@ useGuidely().isFeatureEnabled("new-checkout"); // or getFeatureValue / getFeatur
217
225
 
218
226
  Flags evaluate on the device with `id` (user id, else the anonymous id), `anonymousId`, identify
219
227
  traits, `account`, `$audiences`, `screen`, `path`, `platform` (`ios` / `android`), `deviceType`,
220
- `appVersion`, `locale` and `environment`, plus `flagAttributes` from the provider. The last payload
228
+ `appVersion`, `osVersion`, `locale` and `environment`, plus `flagAttributes` from the provider. The last payload
221
229
  is kept in `storage`, so flags work offline from a cold start; they refresh when the app returns to
222
230
  the foreground. Experiment exposures are tracked as `experiment_viewed` events.
223
231
 
package/dist/index.cjs CHANGED
@@ -46,6 +46,8 @@ __export(index_exports, {
46
46
  TargetRegistry: () => TargetRegistry,
47
47
  createFlagsClient: () => import_flags6.createFlagsClient,
48
48
  createSseParser: () => createSseParser,
49
+ detectAppVersion: () => detectAppVersion,
50
+ detectOsVersion: () => detectOsVersion,
49
51
  guidelyScreenLayout: () => guidelyScreenLayout,
50
52
  htmlToText: () => htmlToText,
51
53
  noopClient: () => noopClient,
@@ -1445,14 +1447,14 @@ function quoteValue(value) {
1445
1447
  function attrSelector(name, value) {
1446
1448
  return `[${name}=${quoteValue(value)}]`.slice(0, 300);
1447
1449
  }
1448
- function cleanText(text) {
1449
- const t = (text ?? "").replace(/\s+/g, " ").trim();
1450
+ function cleanText(text2) {
1451
+ const t = (text2 ?? "").replace(/\s+/g, " ").trim();
1450
1452
  return t ? t.slice(0, 100) : void 0;
1451
1453
  }
1452
1454
  function tapProperties(el, x, y, platform, maskAll) {
1453
1455
  const props = { selector: el.selector.slice(0, 300), tag: el.tag || "View", x: Math.round(x) || 0, y: Math.round(y) || 0, platform };
1454
- const text = maskAll ? void 0 : cleanText(el.text);
1455
- if (text) props.text = text;
1456
+ const text2 = maskAll ? void 0 : cleanText(el.text);
1457
+ if (text2) props.text = text2;
1456
1458
  return props;
1457
1459
  }
1458
1460
  var r2 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 100) / 100;
@@ -2073,10 +2075,10 @@ function xhrStreamPost() {
2073
2075
  const type = xhr.getResponseHeader("content-type") || "";
2074
2076
  if (!type.includes("text/event-stream")) return;
2075
2077
  streamed = true;
2076
- const text = xhr.responseText || "";
2077
- if (text.length > seen) {
2078
- onChunk(text.slice(seen));
2079
- seen = text.length;
2078
+ const text2 = xhr.responseText || "";
2079
+ if (text2.length > seen) {
2080
+ onChunk(text2.slice(seen));
2081
+ seen = text2.length;
2080
2082
  }
2081
2083
  };
2082
2084
  xhr.open("POST", url);
@@ -2151,14 +2153,14 @@ function fetchStreamPost(fetchImpl) {
2151
2153
  if (rest) onChunk(rest);
2152
2154
  return { status: res.status, json: null, streamed: true };
2153
2155
  }
2154
- const text = await race(res.text());
2156
+ const text2 = await race(res.text());
2155
2157
  if (sse) {
2156
- onChunk(text);
2158
+ onChunk(text2);
2157
2159
  return { status: res.status, json: null, streamed: true };
2158
2160
  }
2159
2161
  let json = null;
2160
2162
  try {
2161
- json = JSON.parse(text || "null");
2163
+ json = JSON.parse(text2 || "null");
2162
2164
  } catch {
2163
2165
  json = null;
2164
2166
  }
@@ -2271,10 +2273,10 @@ var Messenger = class {
2271
2273
  if (v.kind === "tab" && v.tab === "messages") void this.refreshConversations();
2272
2274
  }
2273
2275
  /** Opens a new thread (optionally pre-filling the composer). */
2274
- showNewMessage(text = "") {
2276
+ showNewMessage(text2 = "") {
2275
2277
  this.stopThreadPolling();
2276
2278
  if (!this.snapshot.open) this.deps.onOpen();
2277
- this.set({ open: true, view: { kind: "thread", conversationId: null }, thread: { conversation: null, messages: [], streaming: {}, sending: false, error: null, draft: text } });
2279
+ this.set({ open: true, view: { kind: "thread", conversationId: null }, thread: { conversation: null, messages: [], streaming: {}, sending: false, error: null, draft: text2 } });
2278
2280
  }
2279
2281
  async openConversation(id) {
2280
2282
  const known = this.snapshot.conversations.find((c) => c.id === id) ?? null;
@@ -2318,8 +2320,8 @@ var Messenger = class {
2318
2320
  }
2319
2321
  }
2320
2322
  /** Sends the composer text: starts a conversation or posts to the open one. Streams Scout's reply. */
2321
- async send(text) {
2322
- const body = text.trim().slice(0, 4e3);
2323
+ async send(text2) {
2324
+ const body = text2.trim().slice(0, 4e3);
2323
2325
  const thread = this.snapshot.thread;
2324
2326
  if (!body || !thread || thread.sending) return;
2325
2327
  this.setThread({ sending: true, error: null, draft: "" });
@@ -2967,7 +2969,7 @@ var PersistentStore = class {
2967
2969
 
2968
2970
  // src/core/client.ts
2969
2971
  var SDK_NAME = "@guidelytech/react-native";
2970
- var SDK_VERSION = "0.13.0";
2972
+ var SDK_VERSION = "0.15.0";
2971
2973
  var DEFAULT_API_URL = "https://guidelytech.com";
2972
2974
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
2973
2975
  var CACHED_START_MS = 1500;
@@ -3611,7 +3613,7 @@ var GuidelyClient = class {
3611
3613
  const opts = this.options.sessionReplay ?? {};
3612
3614
  const ids = new Set((r?.blockSelectors ?? []).map((sel) => guidelyIdFromSelector(sel)).filter((id) => !!id));
3613
3615
  const rects = await Promise.all(Array.from(ids, (id) => this.targets.measure(id)));
3614
- return { maskAllText: !!opts.maskAllText || !!r?.maskAllText, maskAllImages: !!opts.maskAllImages, blockRects: rects.filter((rect) => !!rect) };
3616
+ return { maskAllText: !!opts.maskAllText || !!r?.maskAllText, maskAllImages: !!opts.maskAllImages || !!r?.maskAllImages, blockRects: rects.filter((rect) => !!rect) };
3615
3617
  }
3616
3618
  /**
3617
3619
  * Records this session even when it was not sampled (still needs `sessionReplay.enabled`, replay
@@ -3739,12 +3741,12 @@ var GuidelyClient = class {
3739
3741
  /** `{ appVersion, build }` of this launch, without unknown keys. */
3740
3742
  appInfo() {
3741
3743
  const appVersion = this.options.appVersion ?? this.bridge.device.appVersion;
3742
- const build = this.options.appBuild;
3744
+ const build = this.options.appBuild ?? (this.options.appVersion ? void 0 : this.bridge.device.appBuild);
3743
3745
  return { ...appVersion ? { appVersion: String(appVersion) } : {}, ...build ? { build: String(build) } : {} };
3744
3746
  }
3745
3747
  /**
3746
3748
  * Once per launch, as soon as a config is known: `app_install` / `app_update` from the stored
3747
- * version (when the app passes `appVersion` and `storage`), then `app_open` with `coldStart: true`.
3749
+ * version (when the app version is known and the app passes `storage`), then `app_open` with `coldStart: true`.
3748
3750
  */
3749
3751
  checkLaunch() {
3750
3752
  if (this.launchChecked || !this.config || this.destroyed || !this.hydrated) return;
@@ -4561,8 +4563,8 @@ var GuidelyClient = class {
4561
4563
  this.messenger?.close();
4562
4564
  }
4563
4565
  /** Opens a new messenger conversation, optionally pre-filling the message. */
4564
- showNewMessage(text) {
4565
- this.whenHydrated(() => this.messenger?.showNewMessage(text));
4566
+ showNewMessage(text2) {
4567
+ this.whenHydrated(() => this.messenger?.showNewMessage(text2));
4566
4568
  }
4567
4569
  // ---------------------------------------------------------------------------
4568
4570
  // Feature flags
@@ -4586,7 +4588,7 @@ var GuidelyClient = class {
4586
4588
  /** Device facts on every screen view, read by the Pages devices / browsers breakdowns. */
4587
4589
  deviceProperties() {
4588
4590
  const d = this.bridge.device;
4589
- return { platform: d.platform, deviceType: d.deviceType, os: d.platform };
4591
+ return { platform: d.platform, deviceType: d.deviceType, os: d.platform, ...d.osVersion ? { osVersion: d.osVersion } : {} };
4590
4592
  }
4591
4593
  /** Attributes flags are evaluated with: identity, traits, account, audiences, screen and device. */
4592
4594
  flagContext() {
@@ -4597,6 +4599,7 @@ var GuidelyClient = class {
4597
4599
  deviceType: d.deviceType,
4598
4600
  locale: d.locale,
4599
4601
  appVersion: this.options.appVersion ?? d.appVersion,
4602
+ osVersion: d.osVersion,
4600
4603
  environment: this.options.environment,
4601
4604
  ...this.screenName ? { screen: this.screenName, path: this.currentUrl() } : {},
4602
4605
  ...this.user?.traits ?? {},
@@ -4687,6 +4690,51 @@ function withoutCode(guide) {
4687
4690
  return { ...guide, config: { ...guide.config, steps: guide.config.steps.map(({ code: _code, ...step }) => step) } };
4688
4691
  }
4689
4692
 
4693
+ // src/core/device.ts
4694
+ function text(value) {
4695
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
4696
+ if (typeof value !== "string") return void 0;
4697
+ const v = value.trim();
4698
+ return v ? v.slice(0, 64) : void 0;
4699
+ }
4700
+ function constant(mod, key) {
4701
+ if (!mod || typeof mod !== "object" && typeof mod !== "function") return void 0;
4702
+ const m = mod;
4703
+ try {
4704
+ const fromGetter = typeof m.getConstants === "function" ? m.getConstants()?.[key] : void 0;
4705
+ return fromGetter ?? m[key];
4706
+ } catch {
4707
+ return void 0;
4708
+ }
4709
+ }
4710
+ function read(mod, versionKey, buildKey) {
4711
+ const appVersion = text(constant(mod, versionKey));
4712
+ if (!appVersion) return null;
4713
+ const appBuild = text(constant(mod, buildKey));
4714
+ return { appVersion, ...appBuild ? { appBuild } : {} };
4715
+ }
4716
+ function inExpoGo(src) {
4717
+ const c = src.expoModules?.ExponentConstants ?? src.nativeModules.ExponentConstants;
4718
+ return constant(c, "executionEnvironment") === "storeClient" || constant(c, "appOwnership") === "expo";
4719
+ }
4720
+ function detectAppVersion(src) {
4721
+ try {
4722
+ const expo = inExpoGo(src) ? null : src.expoModules?.ExpoApplication ?? src.nativeModules.ExpoApplication;
4723
+ return read(src.nativeModules.GuidelyReplay, "appVersion", "appBuild") ?? read(expo, "nativeApplicationVersion", "nativeBuildVersion") ?? read(src.nativeModules.RNDeviceInfo, "appVersion", "buildNumber") ?? {};
4724
+ } catch {
4725
+ return {};
4726
+ }
4727
+ }
4728
+ function detectOsVersion(src) {
4729
+ try {
4730
+ const c = src.platform.constants ?? {};
4731
+ if (src.platform.OS === "android") return text(c.Release) ?? text(src.platform.Version);
4732
+ return text(c.osVersion) ?? text(src.platform.Version);
4733
+ } catch {
4734
+ return void 0;
4735
+ }
4736
+ }
4737
+
4690
4738
  // src/ui/Autocapture.tsx
4691
4739
  var import_react8 = require("react");
4692
4740
  var import_react_native11 = require("react-native");
@@ -4818,8 +4866,8 @@ function describe(f, masked, opts, withText) {
4818
4866
  if (!sub.masked) {
4819
4867
  const hostProps = host?.memoizedProps && typeof host.memoizedProps === "object" ? host.memoizedProps : {};
4820
4868
  const label = props.accessibilityLabel ?? props["aria-label"] ?? hostProps.accessibilityLabel ?? hostProps["aria-label"];
4821
- const text = cleanText(sub.text) ?? (typeof label === "string" ? cleanText(label) : void 0);
4822
- if (text) el.text = text;
4869
+ const text2 = cleanText(sub.text) ?? (typeof label === "string" ? cleanText(label) : void 0);
4870
+ if (text2) el.text = text2;
4823
4871
  }
4824
4872
  }
4825
4873
  if (host) el.measure = () => measureHost(host);
@@ -4975,17 +5023,17 @@ function parseHtml(html) {
4975
5023
  flush();
4976
5024
  current = { kind, level, marker, runs: [] };
4977
5025
  };
4978
- const push = (text) => {
4979
- if (!text) return;
5026
+ const push = (text2) => {
5027
+ if (!text2) return;
4980
5028
  if (!current) current = { kind: "paragraph", level: 1, runs: [] };
4981
- const run = { text };
5029
+ const run = { text: text2 };
4982
5030
  if (style.bold) run.bold = true;
4983
5031
  if (style.italic) run.italic = true;
4984
5032
  if (style.underline) run.underline = true;
4985
5033
  if (style.code) run.code = true;
4986
5034
  if (hrefs.length) run.href = hrefs[hrefs.length - 1];
4987
5035
  const last = current.runs[current.runs.length - 1];
4988
- if (last && last.bold === run.bold && last.italic === run.italic && last.underline === run.underline && last.code === run.code && last.href === run.href) last.text += text;
5036
+ if (last && last.bold === run.bold && last.italic === run.italic && last.underline === run.underline && last.code === run.code && last.href === run.href) last.text += text2;
4989
5037
  else current.runs.push(run);
4990
5038
  };
4991
5039
  const size = (attrs, name) => {
@@ -5001,18 +5049,18 @@ function parseHtml(html) {
5001
5049
  const re = /<!--[\s\S]*?-->|<\/?([a-zA-Z][a-zA-Z0-9]*)\b([^>]*)>|([^<]+|<)/g;
5002
5050
  let m;
5003
5051
  while (m = re.exec(html || "")) {
5004
- const [token, rawTag, attrs = "", text] = m;
5005
- if (text !== void 0) {
5052
+ const [token, rawTag, attrs = "", text2] = m;
5053
+ if (text2 !== void 0) {
5006
5054
  if (skipping) continue;
5007
5055
  if (pdf) {
5008
- if (pdf.inLink) pdf.name += decodeEntities(text);
5056
+ if (pdf.inLink) pdf.name += decodeEntities(text2);
5009
5057
  continue;
5010
5058
  }
5011
5059
  if (fileLink) {
5012
- fileLink.name += decodeEntities(text);
5060
+ fileLink.name += decodeEntities(text2);
5013
5061
  continue;
5014
5062
  }
5015
- push(decodeEntities(text.replace(/\s+/g, " ")));
5063
+ push(decodeEntities(text2.replace(/\s+/g, " ")));
5016
5064
  continue;
5017
5065
  }
5018
5066
  if (!rawTag) continue;
@@ -5586,13 +5634,13 @@ function Conversations({ store, palette: p }) {
5586
5634
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(GButton, { label: "New conversation", palette: p, onPress: () => store.showNewMessage(), style: styles4.fullWidth })
5587
5635
  ] });
5588
5636
  }
5589
- function Bubble({ m, text, name, palette: p, onArticle, onGuide }) {
5637
+ function Bubble({ m, text: text2, name, palette: p, onArticle, onGuide }) {
5590
5638
  if (m.author === "system")
5591
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { accessibilityRole: "text", style: [styles4.system, { color: p.muted }], children: text });
5639
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { accessibilityRole: "text", style: [styles4.system, { color: p.muted }], children: text2 });
5592
5640
  const mine = m.author === "user";
5593
5641
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.View, { style: [styles4.bubbleWrap, mine ? styles4.mine : styles4.theirs], children: [
5594
5642
  !mine ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.author, { color: p.muted }], children: m.author === "agent" ? m.authorName || "Team" : name }) : null,
5595
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.View, { style: [styles4.bubble, { borderRadius: Math.max(12, p.radius) }, mine ? { backgroundColor: p.primary } : { backgroundColor: p.surface }], children: m.status === "pending" && !text ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TypingDots, { color: p.muted }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.bubbleText, { color: mine ? p.onPrimary : p.text }], children: text }) }),
5643
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.View, { style: [styles4.bubble, { borderRadius: Math.max(12, p.radius) }, mine ? { backgroundColor: p.primary } : { backgroundColor: p.surface }], children: m.status === "pending" && !text2 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TypingDots, { color: p.muted }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.bubbleText, { color: mine ? p.onPrimary : p.text }], children: text2 }) }),
5596
5644
  m.citations.length || m.suggestedGuides.length ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.View, { style: styles4.chips, children: [
5597
5645
  m.citations.map((c) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Pressable, { accessibilityRole: "link", accessibilityLabel: `Article: ${c.title}`, onPress: () => onArticle(c.articleId), style: [styles4.chip, { borderColor: p.border }], children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.Text, { style: [styles4.chipText, { color: p.text }], children: [
5598
5646
  "\u{1F4C4} ",
@@ -5611,7 +5659,7 @@ function TypingDots({ color }) {
5611
5659
  function Thread({ store, palette: p, bottomInset }) {
5612
5660
  const snap = useMessenger(store);
5613
5661
  const t = snap.thread;
5614
- const [text, setText] = (0, import_react3.useState)(t?.draft ?? "");
5662
+ const [text2, setText] = (0, import_react3.useState)(t?.draft ?? "");
5615
5663
  const scroll = (0, import_react3.useRef)(null);
5616
5664
  const convo = t?.conversation ?? null;
5617
5665
  (0, import_react3.useEffect)(() => {
@@ -5620,7 +5668,7 @@ function Thread({ store, palette: p, bottomInset }) {
5620
5668
  const streamingIds = (0, import_react3.useMemo)(() => Object.keys(t?.streaming ?? {}).filter((id) => !t?.messages.some((m) => m.id === id)), [t]);
5621
5669
  if (!t) return null;
5622
5670
  const send = () => {
5623
- const value = text;
5671
+ const value = text2;
5624
5672
  setText("");
5625
5673
  void store.send(value);
5626
5674
  };
@@ -5642,7 +5690,7 @@ function Thread({ store, palette: p, bottomInset }) {
5642
5690
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
5643
5691
  import_react_native5.TextInput,
5644
5692
  {
5645
- value: text,
5693
+ value: text2,
5646
5694
  onChangeText: (v) => setText(v.slice(0, 4e3)),
5647
5695
  placeholder: convo ? "Reply\u2026" : "Write a message\u2026",
5648
5696
  placeholderTextColor: p.muted,
@@ -5651,7 +5699,7 @@ function Thread({ store, palette: p, bottomInset }) {
5651
5699
  style: [styles4.input, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(18, p.radius + 6) }]
5652
5700
  }
5653
5701
  ),
5654
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(GButton, { label: "Send", palette: p, onPress: send, disabled: !text.trim() || t.sending })
5702
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(GButton, { label: "Send", palette: p, onPress: send, disabled: !text2.trim() || t.sending })
5655
5703
  ] })
5656
5704
  ] });
5657
5705
  }
@@ -6555,15 +6603,15 @@ function palette(theme, systemScheme, explicit) {
6555
6603
  const wantsDark = theme.dark === "dark" || theme.dark === "auto" && systemScheme === "dark";
6556
6604
  const dark = wantsDark && !explicit?.backgroundColor;
6557
6605
  const background = dark ? DARK.background : theme.backgroundColor ?? "#ffffff";
6558
- const text = dark ? DARK.text : theme.textColor ?? "#111827";
6606
+ const text2 = dark ? DARK.text : theme.textColor ?? "#111827";
6559
6607
  return {
6560
6608
  primary,
6561
6609
  onPrimary: luminance(primary) > 0.45 ? "#111827" : "#ffffff",
6562
6610
  background,
6563
- text,
6564
- muted: dark ? DARK.muted : mix(text, 0.65),
6565
- border: dark ? DARK.border : mix(text, 0.12),
6566
- surface: dark ? DARK.surface : mix(text, 0.05),
6611
+ text: text2,
6612
+ muted: dark ? DARK.muted : mix(text2, 0.65),
6613
+ border: dark ? DARK.border : mix(text2, 0.12),
6614
+ surface: dark ? DARK.surface : mix(text2, 0.05),
6567
6615
  radius: theme.borderRadius ?? 12,
6568
6616
  overlay: `rgba(15, 23, 42, ${theme.overlayOpacity ?? 0.55})`,
6569
6617
  fontFamily: nativeFont(theme.fontFamily)
@@ -6790,14 +6838,25 @@ function deviceLocale() {
6790
6838
  return void 0;
6791
6839
  }
6792
6840
  }
6793
- function defaultBridge(appVersion) {
6841
+ function nativeSources() {
6842
+ return {
6843
+ platform: import_react_native13.Platform,
6844
+ nativeModules: import_react_native13.NativeModules,
6845
+ expoModules: globalThis.expo?.modules
6846
+ };
6847
+ }
6848
+ function defaultBridge(appVersion, appBuild) {
6794
6849
  const { width, height } = import_react_native13.Dimensions.get("window");
6850
+ const sources = nativeSources();
6851
+ const detected = appVersion ? {} : detectAppVersion(sources);
6795
6852
  return {
6796
6853
  device: {
6797
6854
  platform: import_react_native13.Platform.OS === "android" ? "android" : "ios",
6798
6855
  deviceType: import_react_native13.Platform.OS === "ios" && import_react_native13.Platform.isPad || Math.min(width, height) >= 600 ? "tablet" : "mobile",
6799
6856
  locale: deviceLocale(),
6800
- appVersion
6857
+ appVersion: appVersion ?? detected.appVersion,
6858
+ appBuild: appBuild ?? detected.appBuild,
6859
+ osVersion: detectOsVersion(sources)
6801
6860
  },
6802
6861
  openExternal: (url) => void import_react_native13.Linking.openURL(url).catch(() => {
6803
6862
  }),
@@ -6842,7 +6901,7 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
6842
6901
  onNavigate: (url) => optionsRef.current.onNavigate?.(url),
6843
6902
  onError: (err) => optionsRef.current.onError?.(err)
6844
6903
  },
6845
- defaultBridge(latest.appVersion)
6904
+ defaultBridge(latest.appVersion, latest.appBuild)
6846
6905
  );
6847
6906
  } catch (err) {
6848
6907
  optionsRef.current.onError?.(err instanceof Error ? err : new Error(String(err)));
@@ -7045,6 +7104,8 @@ function useActiveGuide() {
7045
7104
  TargetRegistry,
7046
7105
  createFlagsClient,
7047
7106
  createSseParser,
7107
+ detectAppVersion,
7108
+ detectOsVersion,
7048
7109
  guidelyScreenLayout,
7049
7110
  htmlToText,
7050
7111
  noopClient,