@magicfeedback/popup-sdk 1.1.3 → 1.1.5

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.
@@ -195,12 +195,21 @@ interface PopupActions {
195
195
  start?: PopupActionStart;
196
196
  back?: PopupActionDecline;
197
197
  }
198
+ /** Fuente personalizable del popup + survey (viene de la API por popup) */
199
+ interface PopupFont {
200
+ /** Nombre de la familia. Ej: "Inter" (nombre limpio, NO un stack). */
201
+ family: string;
202
+ /** Opcional: URL a un archivo de fuente (woff2/ttf/otf) para armar un @font-face. */
203
+ url?: string;
204
+ }
198
205
  /** Configurable popup styles */
199
206
  interface PopupStyle {
200
207
  theme: 'light' | 'dark';
201
208
  position: 'bottom' | 'bottom-right' | 'bottom-left' | 'top' | 'top-right' | 'top-left' | 'center';
202
209
  /** @deprecated Not used by the SDK; kept only for API compatibility */
203
210
  imageUrl?: string | null;
211
+ /** Fuente personalizada. Si falta, se mantiene el comportamiento actual. */
212
+ font?: PopupFont;
204
213
  }
205
214
 
206
215
  interface PopupRenderer {
@@ -240,6 +249,28 @@ interface ReactNativeErrorUtils {
240
249
  setGlobalHandler(handler: (error: unknown, isFatal?: boolean) => void): void;
241
250
  }
242
251
 
252
+ /**
253
+ * Messaging (#18–22): tracking de notificaciones del host (push / in-app).
254
+ * Un único evento reservado `deepdots_message` con un campo `stage` discriminador;
255
+ * el funnel se correlaciona por `message_id` y se agrupa por `message_title`.
256
+ */
257
+ type MessageStage = 'delivered' | 'clicked' | 'converted';
258
+ interface TrackMessageOptions {
259
+ /** Identificador del mensaje/notificación — correlaciona el funnel delivered→clicked→converted. */
260
+ id: string;
261
+ /** Título del mensaje — dimensión de agrupación de #18–22. */
262
+ title: string;
263
+ /** Canal de entrega. */
264
+ channel: 'push' | 'in_app';
265
+ /** Nombre de la campaña (opcional). */
266
+ campaign?: string;
267
+ /** Valor de conversión (opcional, típico en `stage: 'converted'`). */
268
+ value?: number;
269
+ currency?: string;
270
+ /** Parámetros libres adicionales. */
271
+ params?: Record<string, unknown>;
272
+ }
273
+
243
274
  /**
244
275
  * Main class for managing survey popups
245
276
  */
@@ -311,6 +342,8 @@ declare class DeepdotsPopups {
311
342
  trackFindabilityFriction(frictionTopic: string, params?: Record<string, unknown>): void;
312
343
  /** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
313
344
  trackFunnelStep(funnel: string, step: string, taskId: string, params?: Record<string, unknown>): void;
345
+ /** Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app). No-op si tracking off. */
346
+ trackMessage(stage: MessageStage, options: TrackMessageOptions): void;
314
347
  /**
315
348
  * Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
316
349
  * El host la llama en cada cambio de pantalla; emite `page_view` al salir de la anterior.
@@ -485,7 +518,7 @@ declare class ReactNativePopupRenderer implements PopupRenderer {
485
518
  private partialEmitted;
486
519
  constructor(options?: ReactNativeRendererOptions);
487
520
  init(): void;
488
- show(surveyId: string, productId: string, _actions: PopupActions | undefined, emit: EmitFn, onClose: () => void, env?: string, userId?: string, _style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string): void;
521
+ show(surveyId: string, productId: string, _actions: PopupActions | undefined, emit: EmitFn, onClose: () => void, env?: string, userId?: string, style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string): void;
489
522
  hide(): void;
490
523
  /**
491
524
  * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
@@ -195,12 +195,21 @@ interface PopupActions {
195
195
  start?: PopupActionStart;
196
196
  back?: PopupActionDecline;
197
197
  }
198
+ /** Fuente personalizable del popup + survey (viene de la API por popup) */
199
+ interface PopupFont {
200
+ /** Nombre de la familia. Ej: "Inter" (nombre limpio, NO un stack). */
201
+ family: string;
202
+ /** Opcional: URL a un archivo de fuente (woff2/ttf/otf) para armar un @font-face. */
203
+ url?: string;
204
+ }
198
205
  /** Configurable popup styles */
199
206
  interface PopupStyle {
200
207
  theme: 'light' | 'dark';
201
208
  position: 'bottom' | 'bottom-right' | 'bottom-left' | 'top' | 'top-right' | 'top-left' | 'center';
202
209
  /** @deprecated Not used by the SDK; kept only for API compatibility */
203
210
  imageUrl?: string | null;
211
+ /** Fuente personalizada. Si falta, se mantiene el comportamiento actual. */
212
+ font?: PopupFont;
204
213
  }
205
214
 
206
215
  interface PopupRenderer {
@@ -240,6 +249,28 @@ interface ReactNativeErrorUtils {
240
249
  setGlobalHandler(handler: (error: unknown, isFatal?: boolean) => void): void;
241
250
  }
242
251
 
252
+ /**
253
+ * Messaging (#18–22): tracking de notificaciones del host (push / in-app).
254
+ * Un único evento reservado `deepdots_message` con un campo `stage` discriminador;
255
+ * el funnel se correlaciona por `message_id` y se agrupa por `message_title`.
256
+ */
257
+ type MessageStage = 'delivered' | 'clicked' | 'converted';
258
+ interface TrackMessageOptions {
259
+ /** Identificador del mensaje/notificación — correlaciona el funnel delivered→clicked→converted. */
260
+ id: string;
261
+ /** Título del mensaje — dimensión de agrupación de #18–22. */
262
+ title: string;
263
+ /** Canal de entrega. */
264
+ channel: 'push' | 'in_app';
265
+ /** Nombre de la campaña (opcional). */
266
+ campaign?: string;
267
+ /** Valor de conversión (opcional, típico en `stage: 'converted'`). */
268
+ value?: number;
269
+ currency?: string;
270
+ /** Parámetros libres adicionales. */
271
+ params?: Record<string, unknown>;
272
+ }
273
+
243
274
  /**
244
275
  * Main class for managing survey popups
245
276
  */
@@ -311,6 +342,8 @@ declare class DeepdotsPopups {
311
342
  trackFindabilityFriction(frictionTopic: string, params?: Record<string, unknown>): void;
312
343
  /** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
313
344
  trackFunnelStep(funnel: string, step: string, taskId: string, params?: Record<string, unknown>): void;
345
+ /** Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app). No-op si tracking off. */
346
+ trackMessage(stage: MessageStage, options: TrackMessageOptions): void;
314
347
  /**
315
348
  * Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
316
349
  * El host la llama en cada cambio de pantalla; emite `page_view` al salir de la anterior.
@@ -485,7 +518,7 @@ declare class ReactNativePopupRenderer implements PopupRenderer {
485
518
  private partialEmitted;
486
519
  constructor(options?: ReactNativeRendererOptions);
487
520
  init(): void;
488
- show(surveyId: string, productId: string, _actions: PopupActions | undefined, emit: EmitFn, onClose: () => void, env?: string, userId?: string, _style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string): void;
521
+ show(surveyId: string, productId: string, _actions: PopupActions | undefined, emit: EmitFn, onClose: () => void, env?: string, userId?: string, style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string): void;
489
522
  hide(): void;
490
523
  /**
491
524
  * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
@@ -141,6 +141,43 @@ var init_tracking_manager = __esm({
141
141
  }
142
142
  });
143
143
 
144
+ // src/ui/font.ts
145
+ function fontFormatFromUrl(url) {
146
+ const clean = url.split("?")[0].split("#")[0];
147
+ const ext = clean.slice(clean.lastIndexOf(".") + 1).toLowerCase();
148
+ return FORMAT_BY_EXT[ext];
149
+ }
150
+ function sanitizeFamily(family) {
151
+ return family.replace(/[^A-Za-z0-9 ._-]/g, "").trim();
152
+ }
153
+ function isSafeFontUrl(url) {
154
+ if (!/^(https?:|data:)/i.test(url)) return false;
155
+ return !/[\x00-\x20"<>\\]/.test(url);
156
+ }
157
+ function buildFontFamilyValue(family) {
158
+ const safe = sanitizeFamily(family);
159
+ return `"${safe}", -apple-system, system-ui, sans-serif`;
160
+ }
161
+ function buildFontFaceCss(family, url) {
162
+ if (!url || !isSafeFontUrl(url)) return "";
163
+ const safe = sanitizeFamily(family);
164
+ const fmt = fontFormatFromUrl(url);
165
+ const src = fmt ? `url("${url}") format("${fmt}")` : `url("${url}")`;
166
+ return `@font-face{font-family:"${safe}";src:${src};font-display:swap;}`;
167
+ }
168
+ var FORMAT_BY_EXT;
169
+ var init_font = __esm({
170
+ "src/ui/font.ts"() {
171
+ "use strict";
172
+ FORMAT_BY_EXT = {
173
+ woff2: "woff2",
174
+ woff: "woff",
175
+ ttf: "truetype",
176
+ otf: "opentype"
177
+ };
178
+ }
179
+ });
180
+
144
181
  // src/assets/style.css
145
182
  var style_default;
146
183
  var init_style = __esm({
@@ -285,13 +322,22 @@ var init_style = __esm({
285
322
  padding: 0;
286
323
  font-weight: 900;
287
324
  color: #222;
288
- font-family: 'Montserrat';
325
+ font-family: var(--deepdots-font, 'Montserrat');
289
326
  font-size: 18px;
290
327
  text-transform: uppercase;
291
328
  text-align: center;
292
329
  margin-bottom: 40px;
293
330
  }
294
331
 
332
+ /* Los controles de formulario no heredan font-family por defecto: cuando hay
333
+ fuente custom (clase deepdots-has-font), los forzamos a usar la del contenedor. */
334
+ .deepdots-popup.deepdots-has-font button,
335
+ .deepdots-popup.deepdots-has-font input,
336
+ .deepdots-popup.deepdots-has-font textarea,
337
+ .deepdots-popup.deepdots-has-font select {
338
+ font-family: var(--deepdots-font, inherit);
339
+ }
340
+
295
341
  .deepdots-popup h6 {
296
342
  margin-bottom: 1rem;
297
343
  }
@@ -1064,6 +1110,18 @@ function ensureResponsiveStyles(_popup) {
1064
1110
  `;
1065
1111
  document.head.appendChild(style);
1066
1112
  }
1113
+ function ensureFontFace(family, url) {
1114
+ const css = buildFontFaceCss(family, url);
1115
+ if (!css) return;
1116
+ const STYLE_ID = "deepdots-font-face";
1117
+ let el = document.getElementById(STYLE_ID);
1118
+ if (!el) {
1119
+ el = document.createElement("style");
1120
+ el.id = STYLE_ID;
1121
+ document.head.appendChild(el);
1122
+ }
1123
+ if (el.textContent !== css) el.textContent = css;
1124
+ }
1067
1125
  async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId) {
1068
1126
  let surveyCompletedEmitted = false;
1069
1127
  let stylesInjected = false;
@@ -1099,6 +1157,14 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
1099
1157
  width: 90%;
1100
1158
  min-height: 200px;
1101
1159
  `;
1160
+ const font = style?.font;
1161
+ if (font?.family) {
1162
+ const familyValue = buildFontFamilyValue(font.family);
1163
+ popup.style.fontFamily = familyValue;
1164
+ popup.style.setProperty("--deepdots-font", familyValue);
1165
+ popup.classList.add("deepdots-has-font");
1166
+ if (font.url) ensureFontFace(font.family, font.url);
1167
+ }
1102
1168
  const header = document.createElement("div");
1103
1169
  header.className = "deepdots-popup-header";
1104
1170
  header.style.cssText = "display:flex; justify-content:flex-end; align-items:center; width:100%;";
@@ -1520,6 +1586,7 @@ var init_renderPopup = __esm({
1520
1586
  "src/ui/renderPopup.ts"() {
1521
1587
  "use strict";
1522
1588
  init_tracking_manager();
1589
+ init_font();
1523
1590
  import_native = __toESM(require("@magicfeedback/native"));
1524
1591
  init_style();
1525
1592
  }
@@ -1726,6 +1793,7 @@ function resolveEnvironment(nodeEnv) {
1726
1793
  init_tracking_manager();
1727
1794
 
1728
1795
  // src/ui/surveyHtml.ts
1796
+ init_font();
1729
1797
  function buildSurveyHtml(opts) {
1730
1798
  const env = opts.env === "development" ? "dev" : "prod";
1731
1799
  const version = opts.version ?? "2.2.4";
@@ -1734,10 +1802,12 @@ function buildSurveyHtml(opts) {
1734
1802
  const pid = JSON.stringify(opts.productId);
1735
1803
  const profileJson = JSON.stringify(opts.profile ?? []);
1736
1804
  const metaJson = JSON.stringify(opts.metadata ?? []);
1805
+ const fontFaceCss = opts.font ? buildFontFaceCss(opts.font.family, opts.font.url) : "";
1806
+ const fontFamilyValue = opts.font ? buildFontFamilyValue(opts.font.family) : "-apple-system,system-ui,sans-serif";
1737
1807
  return `<!DOCTYPE html><html><head>
1738
1808
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
1739
1809
  <meta name="color-scheme" content="light"/>
1740
- <style>html,body{margin:0;padding:0;background:transparent;font-family:-apple-system,system-ui,sans-serif}#mf{width:100%;box-sizing:border-box}#mf *{max-width:100%;box-sizing:border-box}</style>
1810
+ <style>${fontFaceCss}html,body{margin:0;padding:0;background:transparent;font-family:${fontFamilyValue}}#mf{width:100%;box-sizing:border-box}#mf *{max-width:100%;box-sizing:border-box}</style>
1741
1811
  </head><body>
1742
1812
  <div id="mf"></div>
1743
1813
  <script>
@@ -1788,13 +1858,13 @@ var ReactNativePopupRenderer = class {
1788
1858
  }
1789
1859
  init() {
1790
1860
  }
1791
- show(surveyId, productId, _actions, emit, onClose, env = "production", userId, _style, sessionId, miniService, analyticsFeedbackSessionId) {
1861
+ show(surveyId, productId, _actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId) {
1792
1862
  this.emitFn = emit;
1793
1863
  this.onCloseFn = onClose;
1794
1864
  this.currentSurveyId = surveyId;
1795
1865
  this.partialEmitted = false;
1796
1866
  const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null, analyticsFeedbackSessionId ?? null);
1797
- const html = buildSurveyHtml({ surveyId, productId, env, profile, metadata });
1867
+ const html = buildSurveyHtml({ surveyId, productId, env, profile, metadata, font: style?.font });
1798
1868
  if (this.options.onShow) {
1799
1869
  this.options.onShow({ surveyId, productId, html });
1800
1870
  } else {
@@ -2460,11 +2530,27 @@ var CrashReporter = class {
2460
2530
  }
2461
2531
  };
2462
2532
 
2533
+ // src/analytics/messaging.ts
2534
+ function buildMessageParams(stage, o) {
2535
+ const p = {
2536
+ stage,
2537
+ message_id: o.id,
2538
+ message_title: o.title,
2539
+ channel: o.channel
2540
+ };
2541
+ if (o.campaign) p.campaign = o.campaign;
2542
+ if (o.value !== void 0) p.value = o.value;
2543
+ if (o.currency) p.currency = o.currency;
2544
+ return { ...p, ...o.params ?? {} };
2545
+ }
2546
+
2463
2547
  // src/core/deepdots-popups.ts
2464
2548
  var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
2465
2549
  var DAY_IN_MS = 24 * 60 * 60 * 1e3;
2466
2550
  var ANALYTICS_MAX_BATCH_SIZE = 20;
2467
2551
  var ANALYTICS_FLUSH_INTERVAL_MS = 3e4;
2552
+ var RESERVED_EVENT_PREFIX = "deepdots_";
2553
+ var CUSTOM_EVENT_PREFIX = "deepdots_event_";
2468
2554
  var DeepdotsPopups = class {
2469
2555
  constructor() {
2470
2556
  this.config = null;
@@ -2617,8 +2703,9 @@ var DeepdotsPopups = class {
2617
2703
  /** Registra un evento de analítica (modelo GA: nombre + params). No-op si tracking off. */
2618
2704
  track(name, params) {
2619
2705
  if (!this.tracking?.isTrackingEnabled()) return;
2620
- this.analytics?.track(name, params);
2621
- this.log("analytics \xB7 track:", name, params ?? {});
2706
+ const eventName = name.startsWith(RESERVED_EVENT_PREFIX) ? name : `${CUSTOM_EVENT_PREFIX}${name}`;
2707
+ this.analytics?.track(eventName, params);
2708
+ this.log("analytics \xB7 track:", eventName, params ?? {});
2622
2709
  }
2623
2710
  /** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
2624
2711
  setUserAttributes(attributes) {
@@ -2670,6 +2757,10 @@ var DeepdotsPopups = class {
2670
2757
  trackFunnelStep(funnel, step, taskId, params) {
2671
2758
  this.track("deepdots_funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
2672
2759
  }
2760
+ /** Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app). No-op si tracking off. */
2761
+ trackMessage(stage, options) {
2762
+ this.track("deepdots_message", buildMessageParams(stage, options));
2763
+ }
2673
2764
  /**
2674
2765
  * Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
2675
2766
  * El host la llama en cada cambio de pantalla; emite `page_view` al salir de la anterior.