@guidelytech/react-native 0.14.0 → 0.16.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
@@ -76,7 +76,7 @@ tasks).
76
76
 
77
77
  | | |
78
78
  |---|---|
79
- | `useGuidely()` | Client: `identify(id, traits?, account?)`, `setAccount`, `updateTraits`, `reset()`, `track(name, props?)`, `screen(name)`, `startGuide(idOrKey)`, `stopGuide()`, `dismissGuide()`, `openChecklist(key)`, `closeChecklist(key)`, `completeChecklistTask(key, taskId)`, `openResourceCenter()`, `closeResourceCenter()`, `refresh()`, `setConsent()`, `on(event, cb)`, `getGuides()`, `getActiveGuide()`, `getState()`, `startSessionRecording()`, `stopSessionRecording()`, `isRecording()`, `getSessionId()` |
79
+ | `useGuidely()` | Client: `identify(id, traits?, account?)`, `setAccount`, `updateTraits`, `reset()`, `track(name, props?)`, `screen(name)`, `startGuide(idOrKey)`, `stopGuide()`, `dismissGuide()`, `openChecklist(key)`, `closeChecklist(key)`, `completeChecklistTask(key, taskId)`, `openResourceCenter()`, `closeResourceCenter()`, `openArticle(articleId)`, `refresh()`, `setConsent()`, `on(event, cb)`, `getGuides()`, `getActiveGuide()`, `getState()`, `startSessionRecording()`, `stopSessionRecording()`, `isRecording()`, `getSessionId()` |
80
80
  | `useGuidelyReady()` | `true` once config is loaded |
81
81
  | `useGuidelyIdentify(id, traits?, account?)` | Identify when the signed-in user changes |
82
82
  | `useGuidelyScreen(name)` | Report a screen view from a component |
package/dist/index.cjs CHANGED
@@ -165,7 +165,7 @@ function FlagsProvider({ children, client: external, payload, ...options }) {
165
165
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FlagsContext.Provider, { value, children });
166
166
  }
167
167
 
168
- // ../shared/dist/chunk-XJXXVV3C.js
168
+ // ../shared/dist/chunk-7PWYIAJI.js
169
169
  function fnv1a(input) {
170
170
  let h = 2166136261;
171
171
  for (let i = 0; i < input.length; i++) {
@@ -574,6 +574,16 @@ function isSafeMediaUrl(url) {
574
574
  return false;
575
575
  }
576
576
  }
577
+ function videoThumbnailUrl(url) {
578
+ const embed = url ? toEmbedUrl(url) : null;
579
+ if (embed?.provider !== "youtube") return null;
580
+ const id = embed.src.split("/embed/")[1]?.split("?")[0];
581
+ return id ? `https://i.ytimg.com/vi/${id}/hqdefault.jpg` : null;
582
+ }
583
+ function stepVideo(step) {
584
+ if (!step.videoUrl || !isSafeMediaUrl(step.videoUrl)) return null;
585
+ return { url: step.videoUrl, poster: (step.imageUrl && isSafeMediaUrl(step.imageUrl) ? step.imageUrl : null) ?? videoThumbnailUrl(step.videoUrl) };
586
+ }
577
587
  var MAX_COLLECTION_DEPTH = 3;
578
588
  function matchesAudiences(required, member) {
579
589
  if (!required || required.length === 0) return true;
@@ -643,6 +653,13 @@ function unreadAnnouncements(items, lastSeenAt) {
643
653
  if (Number.isNaN(seen)) return items.length;
644
654
  return items.filter((a) => Date.parse(a.publishedAt) > seen).length;
645
655
  }
656
+ var ARTICLE_HREF_PREFIX = "#guidely-article:";
657
+ var ARTICLE_ID = /^[A-Za-z0-9_-]{1,64}$/;
658
+ function articleIdFromHref(href) {
659
+ if (!href || !href.startsWith(ARTICLE_HREF_PREFIX)) return null;
660
+ const id = href.slice(ARTICLE_HREF_PREFIX.length);
661
+ return ARTICLE_ID.test(id) ? id : null;
662
+ }
646
663
  var CAPTURE_SCREENSHOT_MAX_BYTES = 8 * 1024 * 1024;
647
664
  var DEFAULT_LOCALE = "en";
648
665
  function normalizeLocale(input) {
@@ -2969,7 +2986,7 @@ var PersistentStore = class {
2969
2986
 
2970
2987
  // src/core/client.ts
2971
2988
  var SDK_NAME = "@guidelytech/react-native";
2972
- var SDK_VERSION = "0.14.0";
2989
+ var SDK_VERSION = "0.16.0";
2973
2990
  var DEFAULT_API_URL = "https://guidelytech.com";
2974
2991
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
2975
2992
  var CACHED_START_MS = 1500;
@@ -3021,6 +3038,8 @@ var GuidelyClient = class {
3021
3038
  __publicField(this, "expanded", /* @__PURE__ */ new Set());
3022
3039
  __publicField(this, "surveyView", null);
3023
3040
  __publicField(this, "resourceCenterOpen", false);
3041
+ /** The article a help-article link asked the resource center to show (`seq` tells repeats apart). */
3042
+ __publicField(this, "resourceCenterArticle", null);
3024
3043
  __publicField(this, "cleanups", []);
3025
3044
  __publicField(this, "timers", /* @__PURE__ */ new Set());
3026
3045
  __publicField(this, "visibilityWatchers", /* @__PURE__ */ new Map());
@@ -3613,7 +3632,7 @@ var GuidelyClient = class {
3613
3632
  const opts = this.options.sessionReplay ?? {};
3614
3633
  const ids = new Set((r?.blockSelectors ?? []).map((sel) => guidelyIdFromSelector(sel)).filter((id) => !!id));
3615
3634
  const rects = await Promise.all(Array.from(ids, (id) => this.targets.measure(id)));
3616
- return { maskAllText: !!opts.maskAllText || !!r?.maskAllText, maskAllImages: !!opts.maskAllImages, blockRects: rects.filter((rect) => !!rect) };
3635
+ return { maskAllText: !!opts.maskAllText || !!r?.maskAllText, maskAllImages: !!opts.maskAllImages || !!r?.maskAllImages, blockRects: rects.filter((rect) => !!rect) };
3617
3636
  }
3618
3637
  /**
3619
3638
  * Records this session even when it was not sampled (still needs `sessionReplay.enabled`, replay
@@ -4186,6 +4205,8 @@ var GuidelyClient = class {
4186
4205
  */
4187
4206
  navigate(url) {
4188
4207
  if (!url) return;
4208
+ const articleId = articleIdFromHref(url);
4209
+ if (articleId) return this.openArticle(articleId);
4189
4210
  try {
4190
4211
  if (this.options.onNavigate?.(url) === true) return;
4191
4212
  if (/^[a-z][a-z0-9+.-]*:/i.test(url)) this.bridge.openExternal(url);
@@ -4209,7 +4230,7 @@ var GuidelyClient = class {
4209
4230
  for (const f of [...this.flows.values(), ...this.active ? [this.active] : []]) if (f.view) steps.push({ ...f.view, expanded: this.expanded.has(f.guide.id) });
4210
4231
  const checklists = [];
4211
4232
  this.checklists.forEach((w, id) => checklists.push({ guide: w.guide, progress: this.getChecklistProgress(id), open: w.open }));
4212
- this.ui = { steps, checklists, survey: this.surveyView, resourceCenterOpen: this.resourceCenterOpen, theme: this.themeFor(), messenger: !!this.messenger };
4233
+ this.ui = { steps, checklists, survey: this.surveyView, resourceCenterOpen: this.resourceCenterOpen, resourceCenterArticle: this.resourceCenterArticle, theme: this.themeFor(), messenger: !!this.messenger };
4213
4234
  for (const cb of Array.from(this.uiListeners)) {
4214
4235
  try {
4215
4236
  cb();
@@ -4229,6 +4250,12 @@ var GuidelyClient = class {
4229
4250
  closeStep(guideId) {
4230
4251
  this.flowFor(guideId)?.controller.dismiss();
4231
4252
  }
4253
+ /** The video on a step was tapped: records `guide_video_clicked`, then opens the video outside the app. */
4254
+ openVideo(guideId, url) {
4255
+ const v = this.flowFor(guideId)?.view;
4256
+ if (v) this.enqueue("guide_video_clicked", { guideId: v.guide.id, guideVersion: v.guide.version, stepId: v.step.id, stepIndex: v.index, properties: { videoUrl: url } });
4257
+ this.navigate(url);
4258
+ }
4232
4259
  /** The dimmed backdrop behind a step was tapped. */
4233
4260
  backdropPressed(guideId) {
4234
4261
  const f = this.flowFor(guideId);
@@ -4501,10 +4528,40 @@ var GuidelyClient = class {
4501
4528
  this.emitter.emit("resource-center:open");
4502
4529
  this.notify();
4503
4530
  }
4531
+ /**
4532
+ * Opens one help article: in Scout's Help tab when the messenger is on, otherwise in the resource
4533
+ * center. Help-article links in guides and articles (`#guidely-article:<id>`) call this.
4534
+ */
4535
+ openArticle(articleId) {
4536
+ if (this.messenger) {
4537
+ this.whenHydrated(() => void this.messenger?.openArticle(articleId));
4538
+ return;
4539
+ }
4540
+ if (!this.getResourceCenter()) {
4541
+ this.log("openArticle: the resource center is not enabled or published for this environment");
4542
+ return;
4543
+ }
4544
+ this.resourceCenterArticle = { id: articleId, seq: (this.resourceCenterArticle?.seq ?? 0) + 1 };
4545
+ if (this.resourceCenterOpen) this.notify();
4546
+ else this.openResourceCenter();
4547
+ }
4548
+ /** A resource-center article with its body: from the config, or fetched when the config left it out. */
4549
+ async loadArticle(articleId) {
4550
+ const listed = this.getResourceCenter()?.articles.find((a) => a.id === articleId);
4551
+ if (listed && !listed.bodyOmitted) return listed;
4552
+ try {
4553
+ const locale = this.articleLocale();
4554
+ const res = await this.api.get(`/articles/${encodeURIComponent(articleId)}`, locale ? { params: { locale } } : {});
4555
+ return res.ok && res.data ? res.data.article : null;
4556
+ } catch {
4557
+ return null;
4558
+ }
4559
+ }
4504
4560
  closeResourceCenter() {
4505
4561
  if (this.messenger) return this.closeMessenger();
4506
4562
  if (!this.resourceCenterOpen) return;
4507
4563
  this.resourceCenterOpen = false;
4564
+ this.resourceCenterArticle = null;
4508
4565
  this.notify();
4509
4566
  }
4510
4567
  // ---------------------------------------------------------------------------
@@ -5850,7 +5907,7 @@ var styles4 = import_react_native5.StyleSheet.create({
5850
5907
  var import_react4 = require("react");
5851
5908
  var import_react_native6 = require("react-native");
5852
5909
  var import_jsx_runtime6 = require("react/jsx-runtime");
5853
- function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
5910
+ function ResourceCenter({ client, palette: p, open, articleRequest, topInset, bottomInset }) {
5854
5911
  const rc = client.getResourceCenter();
5855
5912
  const [query, setQuery] = (0, import_react4.useState)("");
5856
5913
  const [results, setResults] = (0, import_react4.useState)(null);
@@ -5863,6 +5920,19 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
5863
5920
  setArticle(null);
5864
5921
  }
5865
5922
  }, [open]);
5923
+ (0, import_react4.useEffect)(() => {
5924
+ if (!open || !articleRequest) return;
5925
+ let alive = true;
5926
+ void client.loadArticle(articleRequest.id).then((a) => {
5927
+ if (!alive || !a) return;
5928
+ if (a.externalUrl) return client.navigate(a.externalUrl);
5929
+ client.articleViewed(a);
5930
+ setArticle(a);
5931
+ });
5932
+ return () => {
5933
+ alive = false;
5934
+ };
5935
+ }, [client, open, articleRequest?.seq]);
5866
5936
  (0, import_react4.useEffect)(() => {
5867
5937
  const q = query.trim();
5868
5938
  if (q.length < 2) {
@@ -6044,7 +6114,7 @@ function positionCard(target, size, viewport, insets, placement, offset) {
6044
6114
  // src/ui/StepCard.tsx
6045
6115
  var import_react_native7 = require("react-native");
6046
6116
  var import_jsx_runtime7 = (
6047
- // An empty description marks the image as decorative: screen readers skip it.
6117
+ // The video opens outside the app (browser or the provider's app), like a new tab on the web.
6048
6118
  require("react/jsx-runtime")
6049
6119
  );
6050
6120
  function StepCard({ view, client, palette: p, width, style, compact }) {
@@ -6053,16 +6123,33 @@ function StepCard({ view, client, palette: p, width, style, compact }) {
6053
6123
  const body = step.type === "custom" ? step.customHtml || step.body : step.body;
6054
6124
  const showProgress = step.showProgress && view.total > 1;
6055
6125
  const onLink = (url) => client.navigate(url);
6126
+ const video = stepVideo(step);
6056
6127
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Surface, { palette: p, style: [{ width }, style], children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { accessible: false, accessibilityViewIsModal: view.asModal || step.type === "modal", style: compact ? styles6.compact : styles6.inner, children: [
6057
- step.imageUrl ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
6058
- import_react_native7.Image,
6128
+ video ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
6129
+ import_react_native7.Pressable,
6059
6130
  {
6060
- source: { uri: step.imageUrl },
6061
- style: [styles6.image, { borderRadius: Math.max(0, p.radius - 4) }],
6062
- resizeMode: "cover",
6063
- accessibilityIgnoresInvertColors: true,
6064
- ...step.imageAlt?.trim() ? { accessible: true, accessibilityRole: "image", accessibilityLabel: step.imageAlt.trim() } : { accessible: false, importantForAccessibility: "no" }
6131
+ onPress: () => client.openVideo(guide.id, video.url),
6132
+ accessibilityRole: "button",
6133
+ accessibilityLabel: step.title ? `Play video: ${step.title}` : "Play video",
6134
+ accessibilityHint: "Opens the video outside the app",
6135
+ style: [styles6.video, { borderRadius: Math.max(0, p.radius - 4) }],
6136
+ children: [
6137
+ video.poster ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Image, { source: { uri: video.poster }, style: import_react_native7.StyleSheet.absoluteFill, resizeMode: "cover", accessibilityIgnoresInvertColors: true, accessible: false }) : null,
6138
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: styles6.play, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: styles6.playTriangle }) })
6139
+ ]
6065
6140
  }
6141
+ ) : step.imageUrl ? (
6142
+ // An empty description marks the image as decorative: screen readers skip it.
6143
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
6144
+ import_react_native7.Image,
6145
+ {
6146
+ source: { uri: step.imageUrl },
6147
+ style: [styles6.image, { borderRadius: Math.max(0, p.radius - 4) }],
6148
+ resizeMode: "cover",
6149
+ accessibilityIgnoresInvertColors: true,
6150
+ ...step.imageAlt?.trim() ? { accessible: true, accessibilityRole: "image", accessibilityLabel: step.imageAlt.trim() } : { accessible: false, importantForAccessibility: "no" }
6151
+ }
6152
+ )
6066
6153
  ) : null,
6067
6154
  step.title ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: styles6.header, children: [
6068
6155
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { accessibilityRole: "header", style: [styles6.title, { color: p.text, fontFamily: p.fontFamily }], children: step.title }),
@@ -6092,6 +6179,10 @@ var styles6 = import_react_native7.StyleSheet.create({
6092
6179
  inner: { padding: 16, gap: 10 },
6093
6180
  compact: { paddingHorizontal: 14, paddingVertical: 10, gap: 6 },
6094
6181
  image: { width: "100%", height: 140, marginBottom: 2 },
6182
+ video: { width: "100%", aspectRatio: 16 / 9, maxHeight: 200, marginBottom: 2, overflow: "hidden", backgroundColor: "#0f172a", alignItems: "center", justifyContent: "center" },
6183
+ play: { width: 52, height: 52, borderRadius: 26, backgroundColor: "rgba(15,23,42,0.72)", alignItems: "center", justifyContent: "center", paddingLeft: 4 },
6184
+ // A CSS-border triangle: no icon font needed.
6185
+ playTriangle: { width: 0, height: 0, borderTopWidth: 10, borderBottomWidth: 10, borderLeftWidth: 16, borderTopColor: "transparent", borderBottomColor: "transparent", borderLeftColor: "#fff" },
6095
6186
  header: { flexDirection: "row", alignItems: "flex-start", gap: 8 },
6096
6187
  title: { flex: 1, fontSize: 17, fontWeight: "700", lineHeight: 22 },
6097
6188
  flex: { flex: 1 },
@@ -6683,7 +6774,7 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
6683
6774
  ) : null,
6684
6775
  ui.survey ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Survey, { view: ui.survey, client, palette: palette(client.themeFor(ui.survey.guide), scheme), geometry }) : null,
6685
6776
  ui.steps.map((s) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(StepLayer, { view: s, client, palette: palette(client.themeFor(s.guide), scheme, s.guide.config.appearance), geometry }, s.key)),
6686
- messenger ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MessengerSheet, { store: messenger, client, palette: basePalette, topInset: insets.top, bottomInset: insets.bottom }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ResourceCenter, { client, palette: basePalette, open: ui.resourceCenterOpen, topInset: insets.top, bottomInset: insets.bottom })
6777
+ messenger ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MessengerSheet, { store: messenger, client, palette: basePalette, topInset: insets.top, bottomInset: insets.bottom }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ResourceCenter, { client, palette: basePalette, open: ui.resourceCenterOpen, articleRequest: ui.resourceCenterArticle ?? null, topInset: insets.top, bottomInset: insets.bottom })
6687
6778
  ] });
6688
6779
  }
6689
6780
  var styles9 = import_react_native10.StyleSheet.create({