@magicfeedback/popup-sdk 1.7.2 → 1.8.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
@@ -9,7 +9,7 @@ Package name: `@magicfeedback/popup-sdk`
9
9
  - Loads popup definitions from the Deepdots API
10
10
  - Triggers popups by time on page, scroll depth, click, route exit, or host-driven events
11
11
  - Emits lifecycle events so the host app can track popup activity
12
- - Supports route and language targeting through `segments.path` / `segments.lang`
12
+ - Supports route and language targeting through `segments.path` / `segments.excludedPaths` / `segments.lang`
13
13
  - Collects analytics (navigation, engagement, messaging, crashes) and sends them to Deepdots
14
14
  - Falls back to a no-op renderer outside the browser
15
15
 
@@ -345,8 +345,8 @@ Popup definitions use these trigger types:
345
345
  ## Popup Definition Shape
346
346
 
347
347
  The API returns popup definitions with this structure. `triggers` is an array, `time_on_page`
348
- values are in seconds, and `segments.path` accepts full URLs, path fragments such as `/pricing`,
349
- or hash routes such as `/#/home`:
348
+ values are in seconds, and `segments.path` / `segments.excludedPaths` accept full URLs, path
349
+ fragments such as `/pricing`, or hash routes such as `/#/home`:
350
350
 
351
351
  ```ts
352
352
  interface PopupDefinition {
@@ -395,7 +395,8 @@ interface PopupDefinition {
395
395
  };
396
396
  };
397
397
  segments?: {
398
- path?: string[];
398
+ path?: string[]; // routes where it can be shown (empty/absent = all routes)
399
+ excludedPaths?: string[]; // routes where it must NOT be shown; wins over `path`
399
400
  lang?: string[];
400
401
  [key: string]: unknown;
401
402
  };
@@ -431,7 +432,9 @@ interface DeepdotsEvent {
431
432
 
432
433
  ## Important Behavior Notes
433
434
 
434
- - `segments.path` and `segments.lang` are the segments currently evaluated by the SDK runtime. If no language can be resolved, popups with `segments.lang` are shown rather than filtered out.
435
+ - `segments.path`, `segments.excludedPaths`, and `segments.lang` are the segments currently evaluated by the SDK runtime. If no language can be resolved, popups with `segments.lang` are shown rather than filtered out.
436
+ - `segments.excludedPaths` wins over `segments.path`, and applies on its own when `path` is absent. So `{ "path": ["/"], "excludedPaths": ["/cart"] }` means "everywhere except the cart", and `{ "excludedPaths": ["/cart"] }` means the same thing. Both lists use the same matching rules, which includes the substring match for entries starting with `/` — an entry such as `/cart` also excludes `/cart-recovery`, and `/` on its own would exclude every route.
437
+ - For a deferred `exit` popup, `segments.excludedPaths` is checked twice: on the route the popup was queued from, and again on the destination route where it would be painted. `segments.path` is only checked on the first one, because the destination route is different by definition.
435
438
  - `exit` triggers work across anchor navigation, hash navigation, `history.pushState()`, and `history.replaceState()`.
436
439
  - Pending `exit` popups are stored in `sessionStorage` until they are shown or discarded.
437
440
  - The default browser renderer uses Deepdots forms and applies button labels from `actions.accept`, `actions.start`, `actions.back`, and `actions.complete`.
package/dist/index.d.mts CHANGED
@@ -391,6 +391,15 @@ interface PopupStyle {
391
391
  interface PopupSegments {
392
392
  lang?: string[];
393
393
  path?: string[];
394
+ /**
395
+ * App routes where it must NOT be shown. Wins over `path`: a route listed here
396
+ * blocks the popup even if `path` matches it (so `path: ['/']` +
397
+ * `excludedPaths: ['/cart']` = everywhere but the cart).
398
+ * Same matching rules as `path`: an absolute URL matches the full href, an entry
399
+ * starting with `/` matches any href containing it (this is what covers hash
400
+ * routes like `/#/cart`), anything else matches the pathname exactly.
401
+ */
402
+ excludedPaths?: string[];
394
403
  [key: string]: unknown;
395
404
  }
396
405
  /** Full popup definition */
@@ -823,7 +832,27 @@ declare class DeepdotsPopups {
823
832
  private showDefinition;
824
833
  private shouldShow;
825
834
  private matchesSegmentsLang;
835
+ /** `segments.path`: rutas donde el popup PUEDE mostrarse. Sin lista, en todas. */
826
836
  private matchesSegmentsPath;
837
+ /**
838
+ * `segments.excludedPaths`: rutas donde el popup NO debe mostrarse. Gana sobre
839
+ * `segments.path` (`path: ['/']` + `excludedPaths: ['/cart']` = en todas menos el
840
+ * carrito) y se aplica también cuando no hay `path` (= en todas menos las excluidas).
841
+ */
842
+ private isPathExcluded;
843
+ /**
844
+ * Compara UN candidato de `segments.path`/`segments.excludedPaths` con la ruta
845
+ * actual. Mismas reglas para incluir y para excluir:
846
+ * - URL absoluta → href completo exacto
847
+ * - empieza por / → subcadena del href (así entran las rutas con hash: `/#/home`)
848
+ * - resto → pathname exacto
849
+ * Ignora entradas que no sean string: el backend puede colar un `null` en la lista
850
+ * y no puede tumbar la evaluación del trigger entero (mismo fallo que hubo con
851
+ * `segments.lang`).
852
+ */
853
+ private matchesPathCandidate;
854
+ /** href + pathname normalizados de la ruta a evaluar, o null si no hay `window.location`. */
855
+ private resolveCurrentLocation;
827
856
  private evaluateCooldown;
828
857
  private evaluateLegacyCondition;
829
858
  private hasCooldownElapsed;
package/dist/index.d.ts CHANGED
@@ -391,6 +391,15 @@ interface PopupStyle {
391
391
  interface PopupSegments {
392
392
  lang?: string[];
393
393
  path?: string[];
394
+ /**
395
+ * App routes where it must NOT be shown. Wins over `path`: a route listed here
396
+ * blocks the popup even if `path` matches it (so `path: ['/']` +
397
+ * `excludedPaths: ['/cart']` = everywhere but the cart).
398
+ * Same matching rules as `path`: an absolute URL matches the full href, an entry
399
+ * starting with `/` matches any href containing it (this is what covers hash
400
+ * routes like `/#/cart`), anything else matches the pathname exactly.
401
+ */
402
+ excludedPaths?: string[];
394
403
  [key: string]: unknown;
395
404
  }
396
405
  /** Full popup definition */
@@ -823,7 +832,27 @@ declare class DeepdotsPopups {
823
832
  private showDefinition;
824
833
  private shouldShow;
825
834
  private matchesSegmentsLang;
835
+ /** `segments.path`: rutas donde el popup PUEDE mostrarse. Sin lista, en todas. */
826
836
  private matchesSegmentsPath;
837
+ /**
838
+ * `segments.excludedPaths`: rutas donde el popup NO debe mostrarse. Gana sobre
839
+ * `segments.path` (`path: ['/']` + `excludedPaths: ['/cart']` = en todas menos el
840
+ * carrito) y se aplica también cuando no hay `path` (= en todas menos las excluidas).
841
+ */
842
+ private isPathExcluded;
843
+ /**
844
+ * Compara UN candidato de `segments.path`/`segments.excludedPaths` con la ruta
845
+ * actual. Mismas reglas para incluir y para excluir:
846
+ * - URL absoluta → href completo exacto
847
+ * - empieza por / → subcadena del href (así entran las rutas con hash: `/#/home`)
848
+ * - resto → pathname exacto
849
+ * Ignora entradas que no sean string: el backend puede colar un `null` en la lista
850
+ * y no puede tumbar la evaluación del trigger entero (mismo fallo que hubo con
851
+ * `segments.lang`).
852
+ */
853
+ private matchesPathCandidate;
854
+ /** href + pathname normalizados de la ruta a evaluar, o null si no hay `window.location`. */
855
+ private resolveCurrentLocation;
827
856
  private evaluateCooldown;
828
857
  private evaluateLegacyCondition;
829
858
  private hasCooldownElapsed;
package/dist/index.js CHANGED
@@ -2431,9 +2431,9 @@ var init_logo = __esm({
2431
2431
  }
2432
2432
  });
2433
2433
 
2434
- // ../deepdots-popup-sdk/node_modules/@magicfeedback/native/dist/magicfeedback-sdk.browser.js
2434
+ // node_modules/@magicfeedback/native/dist/magicfeedback-sdk.browser.js
2435
2435
  var require_magicfeedback_sdk_browser = __commonJS({
2436
- "../deepdots-popup-sdk/node_modules/@magicfeedback/native/dist/magicfeedback-sdk.browser.js"(exports, module2) {
2436
+ "node_modules/@magicfeedback/native/dist/magicfeedback-sdk.browser.js"(exports, module2) {
2437
2437
  "use strict";
2438
2438
  !(function(e, t) {
2439
2439
  "object" == typeof exports && "object" == typeof module2 ? module2.exports = t() : "function" == typeof define && define.amd ? define([], t) : "object" == typeof exports ? exports.magicfeedback = t() : e.magicfeedback = t();
@@ -7948,6 +7948,9 @@ var DeepdotsPopups = class {
7948
7948
  this.emitEvent("popup_shown", def.surveyId, { popupId: def.id });
7949
7949
  }
7950
7950
  shouldShow(def, pathUrl, skipPathCheck = false) {
7951
+ if (this.isPathExcluded(def, pathUrl)) {
7952
+ return false;
7953
+ }
7951
7954
  if (!skipPathCheck && !this.matchesSegmentsPath(def, pathUrl)) {
7952
7955
  return false;
7953
7956
  }
@@ -7978,40 +7981,85 @@ var DeepdotsPopups = class {
7978
7981
  }
7979
7982
  return matches;
7980
7983
  }
7984
+ /** `segments.path`: rutas donde el popup PUEDE mostrarse. Sin lista, en todas. */
7981
7985
  matchesSegmentsPath(def, pathUrl) {
7982
7986
  const paths = def.segments?.path;
7983
- if (!paths || paths.length === 0) return true;
7984
- if (typeof window === "undefined" || !window.location) {
7987
+ if (!Array.isArray(paths) || paths.length === 0) return true;
7988
+ const location = this.resolveCurrentLocation(pathUrl);
7989
+ if (!location) {
7985
7990
  this.debug("No window.location available for path comparison", { popupId: def.id, paths });
7986
7991
  return true;
7987
7992
  }
7988
- const normalizedHref = this.normalizeUrl(pathUrl || window.location.href || "");
7989
- const currentUrl = this.safeParseUrl(normalizedHref);
7990
- const normalizedPath = this.normalizeUrl(currentUrl?.pathname || window.location.pathname || "");
7991
7993
  const matches = paths.some((rawCandidate) => {
7992
- const candidate = this.normalizeUrl(rawCandidate);
7993
- let match = false;
7994
- if (candidate.startsWith("http://") || candidate.startsWith("https://")) {
7995
- match = normalizedHref === candidate;
7996
- } else if (candidate.startsWith("/")) {
7997
- match = normalizedHref.includes(candidate);
7998
- } else {
7999
- match = normalizedPath === candidate;
8000
- }
7994
+ const match = this.matchesPathCandidate(rawCandidate, location);
8001
7995
  this.debug("Path comparison", {
8002
7996
  popupId: def.id,
8003
- candidate,
8004
- currentPath: normalizedPath,
8005
- currentHref: normalizedHref,
7997
+ candidate: rawCandidate,
7998
+ currentPath: location.path,
7999
+ currentHref: location.href,
8006
8000
  match
8007
8001
  });
8008
8002
  return match;
8009
8003
  });
8010
8004
  if (!matches) {
8011
- this.debug("No path match for popup", { popupId: def.id, paths, currentPath: normalizedPath });
8005
+ this.debug("No path match for popup", { popupId: def.id, paths, currentPath: location.path });
8012
8006
  }
8013
8007
  return matches;
8014
8008
  }
8009
+ /**
8010
+ * `segments.excludedPaths`: rutas donde el popup NO debe mostrarse. Gana sobre
8011
+ * `segments.path` (`path: ['/']` + `excludedPaths: ['/cart']` = en todas menos el
8012
+ * carrito) y se aplica también cuando no hay `path` (= en todas menos las excluidas).
8013
+ */
8014
+ isPathExcluded(def, pathUrl) {
8015
+ const excluded = def.segments?.excludedPaths;
8016
+ if (!Array.isArray(excluded) || excluded.length === 0) return false;
8017
+ const location = this.resolveCurrentLocation(pathUrl);
8018
+ if (!location) {
8019
+ this.debug("No window.location available for excluded path comparison", { popupId: def.id, excluded });
8020
+ return false;
8021
+ }
8022
+ const hit = excluded.find((rawCandidate) => this.matchesPathCandidate(rawCandidate, location));
8023
+ if (hit !== void 0) {
8024
+ this.debug("Popup excluded by path", {
8025
+ popupId: def.id,
8026
+ candidate: hit,
8027
+ currentPath: location.path,
8028
+ currentHref: location.href
8029
+ });
8030
+ return true;
8031
+ }
8032
+ return false;
8033
+ }
8034
+ /**
8035
+ * Compara UN candidato de `segments.path`/`segments.excludedPaths` con la ruta
8036
+ * actual. Mismas reglas para incluir y para excluir:
8037
+ * - URL absoluta → href completo exacto
8038
+ * - empieza por / → subcadena del href (así entran las rutas con hash: `/#/home`)
8039
+ * - resto → pathname exacto
8040
+ * Ignora entradas que no sean string: el backend puede colar un `null` en la lista
8041
+ * y no puede tumbar la evaluación del trigger entero (mismo fallo que hubo con
8042
+ * `segments.lang`).
8043
+ */
8044
+ matchesPathCandidate(rawCandidate, location) {
8045
+ if (typeof rawCandidate !== "string") return false;
8046
+ const candidate = this.normalizeUrl(rawCandidate);
8047
+ if (!candidate) return false;
8048
+ if (candidate.startsWith("http://") || candidate.startsWith("https://")) {
8049
+ return location.href === candidate;
8050
+ }
8051
+ if (candidate.startsWith("/")) {
8052
+ return location.href.includes(candidate);
8053
+ }
8054
+ return location.path === candidate;
8055
+ }
8056
+ /** href + pathname normalizados de la ruta a evaluar, o null si no hay `window.location`. */
8057
+ resolveCurrentLocation(pathUrl) {
8058
+ if (typeof window === "undefined" || !window.location) return null;
8059
+ const href = this.normalizeUrl(pathUrl || window.location.href || "");
8060
+ const parsed = this.safeParseUrl(href);
8061
+ return { href, path: this.normalizeUrl(parsed?.pathname || window.location.pathname || "") };
8062
+ }
8015
8063
  evaluateCooldown(def, condition) {
8016
8064
  switch (condition.answered) {
8017
8065
  case "SHOWED":