@magicfeedback/popup-sdk 1.1.4 → 1.1.6

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.
@@ -105,6 +105,17 @@ interface AnalyticsKeys {
105
105
  * Configuration options for initializing the DeepdotsPopups SDK
106
106
  */
107
107
 
108
+ /**
109
+ * Custom logger to receive the SDK's debug output instead of the console.
110
+ * Only `log` is required; `warn`/`error`/`info` fall back to `log` when omitted.
111
+ * Lets the host pipe debug output to a file, Firebase, etc. `console` satisfies this shape.
112
+ */
113
+ interface DeepdotsLogger {
114
+ log: (...args: unknown[]) => void;
115
+ warn?: (...args: unknown[]) => void;
116
+ error?: (...args: unknown[]) => void;
117
+ info?: (...args: unknown[]) => void;
118
+ }
108
119
  interface DeepdotsInitParams {
109
120
  /** API key for authentication */
110
121
  apiKey?: string;
@@ -112,6 +123,8 @@ interface DeepdotsInitParams {
112
123
  nodeEnv?: 'development' | 'production';
113
124
  /** Enable debug logging */
114
125
  debug?: boolean;
126
+ /** Custom logger for debug output. When set (and `debug` is true), the SDK routes its logs here instead of `console`. Default: `console`. */
127
+ logger?: DeepdotsLogger;
115
128
  /** Optional user id to send with popup events */
116
129
  userId?: string;
117
130
  /** Starts tracking enabled (default) or disabled (e.g. until consent is granted). Equivalent to calling `setTrackingEnabled` after init. */
@@ -195,12 +208,21 @@ interface PopupActions {
195
208
  start?: PopupActionStart;
196
209
  back?: PopupActionDecline;
197
210
  }
211
+ /** Fuente personalizable del popup + survey (viene de la API por popup) */
212
+ interface PopupFont {
213
+ /** Nombre de la familia. Ej: "Inter" (nombre limpio, NO un stack). */
214
+ family: string;
215
+ /** Opcional: URL a un archivo de fuente (woff2/ttf/otf) para armar un @font-face. */
216
+ url?: string;
217
+ }
198
218
  /** Configurable popup styles */
199
219
  interface PopupStyle {
200
220
  theme: 'light' | 'dark';
201
221
  position: 'bottom' | 'bottom-right' | 'bottom-left' | 'top' | 'top-right' | 'top-left' | 'center';
202
222
  /** @deprecated Not used by the SDK; kept only for API compatibility */
203
223
  imageUrl?: string | null;
224
+ /** Fuente personalizada. Si falta, se mantiene el comportamiento actual. */
225
+ font?: PopupFont;
204
226
  }
205
227
 
206
228
  interface PopupRenderer {
@@ -271,6 +293,8 @@ declare class DeepdotsPopups {
271
293
  private triggers;
272
294
  private initialized;
273
295
  private renderer;
296
+ /** Logger para el output de debug. Default `console`; el host puede inyectar el suyo en init(). */
297
+ private logger;
274
298
  private popupContainer;
275
299
  private popupDefinitions;
276
300
  private popupsLoaded;
@@ -509,7 +533,7 @@ declare class ReactNativePopupRenderer implements PopupRenderer {
509
533
  private partialEmitted;
510
534
  constructor(options?: ReactNativeRendererOptions);
511
535
  init(): void;
512
- 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;
536
+ 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;
513
537
  hide(): void;
514
538
  /**
515
539
  * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
@@ -105,6 +105,17 @@ interface AnalyticsKeys {
105
105
  * Configuration options for initializing the DeepdotsPopups SDK
106
106
  */
107
107
 
108
+ /**
109
+ * Custom logger to receive the SDK's debug output instead of the console.
110
+ * Only `log` is required; `warn`/`error`/`info` fall back to `log` when omitted.
111
+ * Lets the host pipe debug output to a file, Firebase, etc. `console` satisfies this shape.
112
+ */
113
+ interface DeepdotsLogger {
114
+ log: (...args: unknown[]) => void;
115
+ warn?: (...args: unknown[]) => void;
116
+ error?: (...args: unknown[]) => void;
117
+ info?: (...args: unknown[]) => void;
118
+ }
108
119
  interface DeepdotsInitParams {
109
120
  /** API key for authentication */
110
121
  apiKey?: string;
@@ -112,6 +123,8 @@ interface DeepdotsInitParams {
112
123
  nodeEnv?: 'development' | 'production';
113
124
  /** Enable debug logging */
114
125
  debug?: boolean;
126
+ /** Custom logger for debug output. When set (and `debug` is true), the SDK routes its logs here instead of `console`. Default: `console`. */
127
+ logger?: DeepdotsLogger;
115
128
  /** Optional user id to send with popup events */
116
129
  userId?: string;
117
130
  /** Starts tracking enabled (default) or disabled (e.g. until consent is granted). Equivalent to calling `setTrackingEnabled` after init. */
@@ -195,12 +208,21 @@ interface PopupActions {
195
208
  start?: PopupActionStart;
196
209
  back?: PopupActionDecline;
197
210
  }
211
+ /** Fuente personalizable del popup + survey (viene de la API por popup) */
212
+ interface PopupFont {
213
+ /** Nombre de la familia. Ej: "Inter" (nombre limpio, NO un stack). */
214
+ family: string;
215
+ /** Opcional: URL a un archivo de fuente (woff2/ttf/otf) para armar un @font-face. */
216
+ url?: string;
217
+ }
198
218
  /** Configurable popup styles */
199
219
  interface PopupStyle {
200
220
  theme: 'light' | 'dark';
201
221
  position: 'bottom' | 'bottom-right' | 'bottom-left' | 'top' | 'top-right' | 'top-left' | 'center';
202
222
  /** @deprecated Not used by the SDK; kept only for API compatibility */
203
223
  imageUrl?: string | null;
224
+ /** Fuente personalizada. Si falta, se mantiene el comportamiento actual. */
225
+ font?: PopupFont;
204
226
  }
205
227
 
206
228
  interface PopupRenderer {
@@ -271,6 +293,8 @@ declare class DeepdotsPopups {
271
293
  private triggers;
272
294
  private initialized;
273
295
  private renderer;
296
+ /** Logger para el output de debug. Default `console`; el host puede inyectar el suyo en init(). */
297
+ private logger;
274
298
  private popupContainer;
275
299
  private popupDefinitions;
276
300
  private popupsLoaded;
@@ -509,7 +533,7 @@ declare class ReactNativePopupRenderer implements PopupRenderer {
509
533
  private partialEmitted;
510
534
  constructor(options?: ReactNativeRendererOptions);
511
535
  init(): void;
512
- 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;
536
+ 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;
513
537
  hide(): void;
514
538
  /**
515
539
  * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
@@ -141,6 +141,64 @@ 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
+
181
+ // src/util/logger.ts
182
+ function setLogger(logger) {
183
+ active = logger ?? console;
184
+ }
185
+ function sdkLog(...args) {
186
+ active.log(...args);
187
+ }
188
+ function sdkWarn(...args) {
189
+ (active.warn ?? active.log)(...args);
190
+ }
191
+ function sdkError(...args) {
192
+ (active.error ?? active.log)(...args);
193
+ }
194
+ var active;
195
+ var init_logger = __esm({
196
+ "src/util/logger.ts"() {
197
+ "use strict";
198
+ active = console;
199
+ }
200
+ });
201
+
144
202
  // src/assets/style.css
145
203
  var style_default;
146
204
  var init_style = __esm({
@@ -285,13 +343,22 @@ var init_style = __esm({
285
343
  padding: 0;
286
344
  font-weight: 900;
287
345
  color: #222;
288
- font-family: 'Montserrat';
346
+ font-family: var(--deepdots-font, 'Montserrat');
289
347
  font-size: 18px;
290
348
  text-transform: uppercase;
291
349
  text-align: center;
292
350
  margin-bottom: 40px;
293
351
  }
294
352
 
353
+ /* Los controles de formulario no heredan font-family por defecto: cuando hay
354
+ fuente custom (clase deepdots-has-font), los forzamos a usar la del contenedor. */
355
+ .deepdots-popup.deepdots-has-font button,
356
+ .deepdots-popup.deepdots-has-font input,
357
+ .deepdots-popup.deepdots-has-font textarea,
358
+ .deepdots-popup.deepdots-has-font select {
359
+ font-family: var(--deepdots-font, inherit);
360
+ }
361
+
295
362
  .deepdots-popup h6 {
296
363
  margin-bottom: 1rem;
297
364
  }
@@ -1064,6 +1131,18 @@ function ensureResponsiveStyles(_popup) {
1064
1131
  `;
1065
1132
  document.head.appendChild(style);
1066
1133
  }
1134
+ function ensureFontFace(family, url) {
1135
+ const css = buildFontFaceCss(family, url);
1136
+ if (!css) return;
1137
+ const STYLE_ID = "deepdots-font-face";
1138
+ let el = document.getElementById(STYLE_ID);
1139
+ if (!el) {
1140
+ el = document.createElement("style");
1141
+ el.id = STYLE_ID;
1142
+ document.head.appendChild(el);
1143
+ }
1144
+ if (el.textContent !== css) el.textContent = css;
1145
+ }
1067
1146
  async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId) {
1068
1147
  let surveyCompletedEmitted = false;
1069
1148
  let stylesInjected = false;
@@ -1099,6 +1178,14 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
1099
1178
  width: 90%;
1100
1179
  min-height: 200px;
1101
1180
  `;
1181
+ const font = style?.font;
1182
+ if (font?.family) {
1183
+ const familyValue = buildFontFamilyValue(font.family);
1184
+ popup.style.fontFamily = familyValue;
1185
+ popup.style.setProperty("--deepdots-font", familyValue);
1186
+ popup.classList.add("deepdots-has-font");
1187
+ if (font.url) ensureFontFace(font.family, font.url);
1188
+ }
1102
1189
  const header = document.createElement("div");
1103
1190
  header.className = "deepdots-popup-header";
1104
1191
  header.style.cssText = "display:flex; justify-content:flex-end; align-items:center; width:100%;";
@@ -1365,7 +1452,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
1365
1452
  let formInstance = null;
1366
1453
  try {
1367
1454
  if (!import_native.default || typeof import_native.default.form !== "function") {
1368
- console.warn("[MagicFeedback] form() no disponible. Fallback manual.");
1455
+ sdkWarn("[MagicFeedback] form() no disponible. Fallback manual.");
1369
1456
  setLoading(false);
1370
1457
  return;
1371
1458
  }
@@ -1452,7 +1539,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
1452
1539
  }
1453
1540
  }
1454
1541
  if (s.startMessage && s.startMessage !== "") {
1455
- console.log(s.startMessage);
1542
+ sdkLog(s.startMessage);
1456
1543
  updateButtons("start");
1457
1544
  } else {
1458
1545
  updateButtons("in_progress_first");
@@ -1507,11 +1594,11 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
1507
1594
  }
1508
1595
  };
1509
1596
  formInstance.generate(formDivId, generateOptions).catch((err) => {
1510
- console.error("[MagicFeedback] Error generating form:", err);
1597
+ sdkError("[MagicFeedback] Error generating form:", err);
1511
1598
  setLoading(false);
1512
1599
  });
1513
1600
  } catch (e) {
1514
- console.error("[MagicFeedback] Exception initializing form:", e);
1601
+ sdkError("[MagicFeedback] Exception initializing form:", e);
1515
1602
  setLoading(false);
1516
1603
  }
1517
1604
  }
@@ -1520,6 +1607,8 @@ var init_renderPopup = __esm({
1520
1607
  "src/ui/renderPopup.ts"() {
1521
1608
  "use strict";
1522
1609
  init_tracking_manager();
1610
+ init_font();
1611
+ init_logger();
1523
1612
  import_native = __toESM(require("@magicfeedback/native"));
1524
1613
  init_style();
1525
1614
  }
@@ -1726,6 +1815,7 @@ function resolveEnvironment(nodeEnv) {
1726
1815
  init_tracking_manager();
1727
1816
 
1728
1817
  // src/ui/surveyHtml.ts
1818
+ init_font();
1729
1819
  function buildSurveyHtml(opts) {
1730
1820
  const env = opts.env === "development" ? "dev" : "prod";
1731
1821
  const version = opts.version ?? "2.2.4";
@@ -1734,10 +1824,12 @@ function buildSurveyHtml(opts) {
1734
1824
  const pid = JSON.stringify(opts.productId);
1735
1825
  const profileJson = JSON.stringify(opts.profile ?? []);
1736
1826
  const metaJson = JSON.stringify(opts.metadata ?? []);
1827
+ const fontFaceCss = opts.font ? buildFontFaceCss(opts.font.family, opts.font.url) : "";
1828
+ const fontFamilyValue = opts.font ? buildFontFamilyValue(opts.font.family) : "-apple-system,system-ui,sans-serif";
1737
1829
  return `<!DOCTYPE html><html><head>
1738
1830
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
1739
1831
  <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>
1832
+ <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
1833
  </head><body>
1742
1834
  <div id="mf"></div>
1743
1835
  <script>
@@ -1778,6 +1870,7 @@ function buildSurveyHtml(opts) {
1778
1870
  }
1779
1871
 
1780
1872
  // src/platform/react-native-renderer.ts
1873
+ init_logger();
1781
1874
  var ReactNativePopupRenderer = class {
1782
1875
  constructor(options = {}) {
1783
1876
  this.options = options;
@@ -1788,17 +1881,17 @@ var ReactNativePopupRenderer = class {
1788
1881
  }
1789
1882
  init() {
1790
1883
  }
1791
- show(surveyId, productId, _actions, emit, onClose, env = "production", userId, _style, sessionId, miniService, analyticsFeedbackSessionId) {
1884
+ show(surveyId, productId, _actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId) {
1792
1885
  this.emitFn = emit;
1793
1886
  this.onCloseFn = onClose;
1794
1887
  this.currentSurveyId = surveyId;
1795
1888
  this.partialEmitted = false;
1796
1889
  const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null, analyticsFeedbackSessionId ?? null);
1797
- const html = buildSurveyHtml({ surveyId, productId, env, profile, metadata });
1890
+ const html = buildSurveyHtml({ surveyId, productId, env, profile, metadata, font: style?.font });
1798
1891
  if (this.options.onShow) {
1799
1892
  this.options.onShow({ surveyId, productId, html });
1800
1893
  } else {
1801
- console.warn(
1894
+ sdkWarn(
1802
1895
  "[Deepdots] ReactNativePopupRenderer sin onShow: pasa { onShow } a new ReactNativePopupRenderer({...}) para montar el WebView del survey."
1803
1896
  );
1804
1897
  }
@@ -1915,12 +2008,13 @@ function createDefaultRenderer() {
1915
2008
  init_tracking_manager();
1916
2009
 
1917
2010
  // src/analytics/analytics-manager.ts
1918
- var dryRunSink = (payload) => {
1919
- console.log(
2011
+ function createDryRunSink(log = console.log) {
2012
+ return (payload) => log(
1920
2013
  "[DeepdotsAnalytics] (dry-run \xB7 NO enviado \xB7 sin init.analytics) POST /sdk/feedback \u2192",
1921
2014
  JSON.stringify(payload, null, 2)
1922
2015
  );
1923
- };
2016
+ }
2017
+ var dryRunSink = createDryRunSink();
1924
2018
  var AnalyticsManager = class {
1925
2019
  constructor(options = {}) {
1926
2020
  this.events = [];
@@ -2436,7 +2530,7 @@ var CrashReporter = class {
2436
2530
  }
2437
2531
  /** Instala los handlers globales de errores no capturados (no-op sin `window`). */
2438
2532
  install() {
2439
- if (typeof window === "undefined") return;
2533
+ if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
2440
2534
  window.addEventListener("error", (e) => {
2441
2535
  if (!this.isEnabled()) return;
2442
2536
  this.captureUnhandled(e.error ?? e.message ?? "error");
@@ -2460,6 +2554,9 @@ var CrashReporter = class {
2460
2554
  }
2461
2555
  };
2462
2556
 
2557
+ // src/core/deepdots-popups.ts
2558
+ init_logger();
2559
+
2463
2560
  // src/analytics/messaging.ts
2464
2561
  function buildMessageParams(stage, o) {
2465
2562
  const p = {
@@ -2488,6 +2585,8 @@ var DeepdotsPopups = class {
2488
2585
  this.triggers = [];
2489
2586
  this.initialized = false;
2490
2587
  this.renderer = createDefaultRenderer();
2588
+ /** Logger para el output de debug. Default `console`; el host puede inyectar el suyo en init(). */
2589
+ this.logger = console;
2491
2590
  this.popupContainer = null;
2492
2591
  // deprecated: mantenido para compatibilidad interna
2493
2592
  this.popupDefinitions = [];
@@ -2523,6 +2622,8 @@ var DeepdotsPopups = class {
2523
2622
  }
2524
2623
  /** Initialize the SDK with configuration */
2525
2624
  init(config) {
2625
+ this.logger = config.logger ?? console;
2626
+ setLogger(this.logger);
2526
2627
  if (this.initialized) {
2527
2628
  this.log("SDK already initialized");
2528
2629
  return;
@@ -2562,7 +2663,7 @@ var DeepdotsPopups = class {
2562
2663
  onSessionId: (id) => {
2563
2664
  this.analyticsFeedbackSessionId = id;
2564
2665
  }
2565
- }) : void 0;
2666
+ }) : createDryRunSink((...a) => this.logger.log(...a));
2566
2667
  const device = config.device ?? collectDeviceInfo(config.appVersion);
2567
2668
  this.analytics = new AnalyticsManager({
2568
2669
  sink: analyticsSink,
@@ -3271,7 +3372,8 @@ var DeepdotsPopups = class {
3271
3372
  try {
3272
3373
  listener(event);
3273
3374
  } catch (error) {
3274
- console.error("Error in event listener:", error);
3375
+ if (this.logger.error) this.logger.error("Error in event listener:", error);
3376
+ else this.logger.log("Error in event listener:", error);
3275
3377
  }
3276
3378
  });
3277
3379
  }
@@ -3279,7 +3381,7 @@ var DeepdotsPopups = class {
3279
3381
  /** Log debug messages */
3280
3382
  log(...args) {
3281
3383
  if (this.config?.debug) {
3282
- console.log("[DeepdotsPopups]", ...args);
3384
+ this.logger.log("[DeepdotsPopups]", ...args);
3283
3385
  }
3284
3386
  }
3285
3387
  /** External debug method for triggers */