@magicfeedback/popup-sdk 1.3.0 → 1.4.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.
@@ -162,6 +162,15 @@ interface DeepdotsInitParams {
162
162
  * `setContactAttributes` after init (sends only when values changed from the last sync).
163
163
  */
164
164
  contactAttributes?: Record<string, string | number | boolean>;
165
+ /**
166
+ * Whether the SDK renders the popup "modal" (card + backdrop + positioning) around the survey.
167
+ * Default `true`. Only affects React Native (the survey WebView HTML); the web DOM popup is
168
+ * unaffected. With `false` the survey is delivered transparent and edge-to-edge so the host
169
+ * mounts its own Modal/container and controls the styling — avoids the "double modal" when the
170
+ * host already wraps the WebView. The survey stays fully functional (bridge, form, nav buttons,
171
+ * close). Ver `ReactNativePopupRenderer`.
172
+ */
173
+ renderChrome?: boolean;
165
174
  }
166
175
  /**
167
176
  * Options for configuring survey triggers
@@ -243,7 +252,7 @@ interface PopupRenderer {
243
252
  /** Preparar recursos si aplica */
244
253
  init?(): void;
245
254
  /** Mostrar popup */
246
- show(surveyId: string, productId: string, actions: PopupActions | undefined, emit: (type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>) => void, onClose: () => void, env?: string, userId?: string, style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string): void;
255
+ show(surveyId: string, productId: string, actions: PopupActions | undefined, emit: (type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>) => void, onClose: () => void, env?: string, userId?: string, style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string, renderChrome?: boolean): void;
247
256
  /** Ocultar popup */
248
257
  hide(): void;
249
258
  }
@@ -323,6 +332,8 @@ declare class DeepdotsPopups {
323
332
  private deferredExitTimers;
324
333
  private baseUrl;
325
334
  private env;
335
+ /** Si el SDK pinta el "modal" del popup (RN). Default true; false = el host lo envuelve. */
336
+ private renderChrome;
326
337
  /** Identidad + sesión (Fase 1 tracking). Null hasta init(). */
327
338
  private tracking;
328
339
  /** Capa de analytics (canal separado del feedback). Null hasta init(). */
@@ -394,6 +405,15 @@ declare class DeepdotsPopups {
394
405
  trackFindabilityFriction(frictionTopic: string, params?: Record<string, unknown>): void;
395
406
  /** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
396
407
  trackFunnelStep(funnel: string, step: string, taskId: string, params?: Record<string, unknown>): void;
408
+ /**
409
+ * Product Effectiveness: una interacción significativa del usuario (`interaction_type` es la
410
+ * dimensión de agrupación, p. ej. `get_help`, `homepage`, `contact_support`).
411
+ *
412
+ * Existe como evento RESERVADO para que la convención no la invente cada host: un `track()`
413
+ * custom acaba como `deepdots_event_…` y no lo cuenta la analítica de effectiveness.
414
+ * Un `interactionType` vacío se descarta con warning (no aporta dato agrupable).
415
+ */
416
+ trackMeaningfulInteraction(interactionType: string, params?: Record<string, unknown>): void;
397
417
  /**
398
418
  * Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app).
399
419
  * No-op si tracking off. El evento se descarta (con warning) si el `channel` no es válido,
@@ -612,7 +632,7 @@ declare class ReactNativePopupRenderer implements PopupRenderer {
612
632
  private partialEmitted;
613
633
  constructor(options?: ReactNativeRendererOptions);
614
634
  init(): void;
615
- 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;
635
+ show(surveyId: string, productId: string, actions: PopupActions | undefined, emit: EmitFn, onClose: () => void, env?: string, userId?: string, style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string, renderChrome?: boolean): void;
616
636
  hide(): void;
617
637
  /**
618
638
  * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
@@ -162,6 +162,15 @@ interface DeepdotsInitParams {
162
162
  * `setContactAttributes` after init (sends only when values changed from the last sync).
163
163
  */
164
164
  contactAttributes?: Record<string, string | number | boolean>;
165
+ /**
166
+ * Whether the SDK renders the popup "modal" (card + backdrop + positioning) around the survey.
167
+ * Default `true`. Only affects React Native (the survey WebView HTML); the web DOM popup is
168
+ * unaffected. With `false` the survey is delivered transparent and edge-to-edge so the host
169
+ * mounts its own Modal/container and controls the styling — avoids the "double modal" when the
170
+ * host already wraps the WebView. The survey stays fully functional (bridge, form, nav buttons,
171
+ * close). Ver `ReactNativePopupRenderer`.
172
+ */
173
+ renderChrome?: boolean;
165
174
  }
166
175
  /**
167
176
  * Options for configuring survey triggers
@@ -243,7 +252,7 @@ interface PopupRenderer {
243
252
  /** Preparar recursos si aplica */
244
253
  init?(): void;
245
254
  /** Mostrar popup */
246
- show(surveyId: string, productId: string, actions: PopupActions | undefined, emit: (type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>) => void, onClose: () => void, env?: string, userId?: string, style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string): void;
255
+ show(surveyId: string, productId: string, actions: PopupActions | undefined, emit: (type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>) => void, onClose: () => void, env?: string, userId?: string, style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string, renderChrome?: boolean): void;
247
256
  /** Ocultar popup */
248
257
  hide(): void;
249
258
  }
@@ -323,6 +332,8 @@ declare class DeepdotsPopups {
323
332
  private deferredExitTimers;
324
333
  private baseUrl;
325
334
  private env;
335
+ /** Si el SDK pinta el "modal" del popup (RN). Default true; false = el host lo envuelve. */
336
+ private renderChrome;
326
337
  /** Identidad + sesión (Fase 1 tracking). Null hasta init(). */
327
338
  private tracking;
328
339
  /** Capa de analytics (canal separado del feedback). Null hasta init(). */
@@ -394,6 +405,15 @@ declare class DeepdotsPopups {
394
405
  trackFindabilityFriction(frictionTopic: string, params?: Record<string, unknown>): void;
395
406
  /** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
396
407
  trackFunnelStep(funnel: string, step: string, taskId: string, params?: Record<string, unknown>): void;
408
+ /**
409
+ * Product Effectiveness: una interacción significativa del usuario (`interaction_type` es la
410
+ * dimensión de agrupación, p. ej. `get_help`, `homepage`, `contact_support`).
411
+ *
412
+ * Existe como evento RESERVADO para que la convención no la invente cada host: un `track()`
413
+ * custom acaba como `deepdots_event_…` y no lo cuenta la analítica de effectiveness.
414
+ * Un `interactionType` vacío se descarta con warning (no aporta dato agrupable).
415
+ */
416
+ trackMeaningfulInteraction(interactionType: string, params?: Record<string, unknown>): void;
397
417
  /**
398
418
  * Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app).
399
419
  * No-op si tracking off. El evento se descarta (con warning) si el `channel` no es válido,
@@ -612,7 +632,7 @@ declare class ReactNativePopupRenderer implements PopupRenderer {
612
632
  private partialEmitted;
613
633
  constructor(options?: ReactNativeRendererOptions);
614
634
  init(): void;
615
- 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;
635
+ show(surveyId: string, productId: string, actions: PopupActions | undefined, emit: EmitFn, onClose: () => void, env?: string, userId?: string, style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string, renderChrome?: boolean): void;
616
636
  hide(): void;
617
637
  /**
618
638
  * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
@@ -614,7 +614,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
614
614
  };
615
615
  generateOptions.onLoadedEvent = ({ formData }) => {
616
616
  try {
617
- } catch (e) {
617
+ } catch {
618
618
  }
619
619
  const s = formData?.style;
620
620
  if (s && !stylesInjected) {
@@ -779,6 +779,10 @@ var POPUP_TRIGGER_CONDITION_STATUSES = [
779
779
  ];
780
780
 
781
781
  // src/triggers/index.ts
782
+ function toNumericValue(value, fallback) {
783
+ const n = Number(value);
784
+ return Number.isFinite(n) ? n : fallback;
785
+ }
782
786
  function setupTrigger(popups, trigger) {
783
787
  switch (trigger.type) {
784
788
  case "time":
@@ -799,14 +803,14 @@ function setupTrigger(popups, trigger) {
799
803
  }
800
804
  }
801
805
  function setupTimeTrigger(popups, trigger) {
802
- const delay = trigger.value || 5e3;
806
+ const delay = toNumericValue(trigger.value, 5e3);
803
807
  setTimeout(() => {
804
808
  popups.triggerSurvey(trigger.surveyId, trigger.popupId);
805
809
  }, delay);
806
810
  popups.debug(`Time trigger set for ${delay}ms`);
807
811
  }
808
812
  function setupScrollTrigger(popups, trigger) {
809
- const threshold = trigger.value || 50;
813
+ const threshold = toNumericValue(trigger.value, 50);
810
814
  const handler = () => {
811
815
  const scrollPercentage = window.scrollY / (document.documentElement.scrollHeight - window.innerHeight) * 100;
812
816
  if (scrollPercentage >= threshold) {
@@ -986,6 +990,16 @@ function buildSurveyHtml(opts) {
986
990
  const colorScheme = isDark ? "dark" : "light";
987
991
  const textPrimary = isDark ? "#f0f0f0" : "#111";
988
992
  const pos = POSITION_MAP[opts.position ?? "center"] ?? POSITION_MAP.center;
993
+ const chrome = opts.chrome !== false;
994
+ const bodyBg = chrome ? pos.background : "transparent";
995
+ const bodyPadding = chrome ? pos.padding : "0";
996
+ const bodyDisplay = chrome ? "flex" : "block";
997
+ const cardBg = chrome ? popupBg : "transparent";
998
+ const cardShadow = chrome ? "0 4px 6px rgba(0,0,0,0.1)" : "none";
999
+ const cardRadius = chrome ? "8px" : "0";
1000
+ const cardMaxWidth = chrome ? "600px" : "none";
1001
+ const cardWidth = chrome ? "90%" : "100%";
1002
+ const cardMinHeight = chrome ? "200px" : "0";
989
1003
  const backLabel = JSON.stringify(opts.actions?.back?.label ?? "Back");
990
1004
  const startLabel = JSON.stringify(opts.actions?.start?.label ?? "Start survey");
991
1005
  const completeLabel = JSON.stringify(opts.actions?.complete?.label ?? "Complete survey");
@@ -997,8 +1011,8 @@ function buildSurveyHtml(opts) {
997
1011
  ${style_default}
998
1012
  ${fontFaceCss}
999
1013
  html,body{margin:0;padding:0;height:100%;font-family:${fontFamilyValue}}
1000
- body{display:flex;justify-content:${pos.justifyContent};align-items:${pos.alignItems};background:${pos.background};padding:${pos.padding};box-sizing:border-box}
1001
- .deepdots-popup{position:relative;display:flex;flex-direction:column;justify-content:flex-start;background:${popupBg};color-scheme:${colorScheme};border-radius:8px;padding:24px;box-shadow:0 4px 6px rgba(0,0,0,0.1);max-width:600px;width:90%;min-height:200px;box-sizing:border-box}
1014
+ body{display:${bodyDisplay};justify-content:${pos.justifyContent};align-items:${pos.alignItems};background:${bodyBg};padding:${bodyPadding};box-sizing:border-box}
1015
+ .deepdots-popup{position:relative;display:flex;flex-direction:column;justify-content:flex-start;background:${cardBg};color-scheme:${colorScheme};border-radius:${cardRadius};padding:24px;box-shadow:${cardShadow};max-width:${cardMaxWidth};width:${cardWidth};min-height:${cardMinHeight};box-sizing:border-box}
1002
1016
  .deepdots-popup-header{display:flex;justify-content:flex-end;align-items:center;width:100%}
1003
1017
  #dd-close{background:transparent;border:none;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:8px;cursor:pointer;color:${textPrimary};padding:4px}
1004
1018
  .deepdots-popup-container-content{display:flex;flex-direction:column;padding:0 20px 12px 20px;max-height:80vh;overflow:hidden;box-sizing:border-box}
@@ -1056,6 +1070,7 @@ body{display:flex;justify-content:${pos.justifyContent};align-items:${pos.alignI
1056
1070
  function emit(s){ try{ if(window.ReactNativeWebView){ window.ReactNativeWebView.postMessage(s); } }catch(e){} }
1057
1071
  function emitJSON(n,p){ try{ emit(JSON.stringify({name:n,payload:p||{}})); }catch(e){} }
1058
1072
  window.DeepdotsClose = function(){ emitJSON('popup_close'); };
1073
+ var chromeOn=${chrome ? "true" : "false"};
1059
1074
 
1060
1075
  var popup=document.getElementById('dd-popup');
1061
1076
  var main=document.getElementById('dd-main');
@@ -1131,7 +1146,7 @@ body{display:flex;justify-content:${pos.justifyContent};align-items:${pos.alignI
1131
1146
  var s=args && args.formData ? args.formData.style : null;
1132
1147
  if(s && !stylesInjected){
1133
1148
  stylesInjected=true;
1134
- if(s.boxBackgroundColor){ popup.style.background=s.boxBackgroundColor; }
1149
+ if(chromeOn && s.boxBackgroundColor){ popup.style.background=s.boxBackgroundColor; }
1135
1150
  if(s.contentAlign){ main.style.justifyContent = s.contentAlign==='center' ? 'center' : 'flex-start'; }
1136
1151
  if(s.buttonPrimaryColor){
1137
1152
  submitBtn.style.background=s.buttonPrimaryColor; submitBtn.style.border='none'; submitBtn.style.color='#fff';
@@ -1221,7 +1236,7 @@ var ReactNativePopupRenderer = class {
1221
1236
  }
1222
1237
  init() {
1223
1238
  }
1224
- show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId) {
1239
+ show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId, renderChrome) {
1225
1240
  this.emitFn = emit;
1226
1241
  this.onCloseFn = onClose;
1227
1242
  this.currentSurveyId = surveyId;
@@ -1236,7 +1251,9 @@ var ReactNativePopupRenderer = class {
1236
1251
  font: style?.font,
1237
1252
  theme: style?.theme,
1238
1253
  position: style?.position,
1239
- actions
1254
+ actions,
1255
+ // renderChrome:false (init) → HTML sin tarjeta/overlay: el host lo envuelve en su Modal.
1256
+ chrome: renderChrome
1240
1257
  });
1241
1258
  if (this.options.onShow) {
1242
1259
  this.options.onShow({ surveyId, productId, html });
@@ -1327,7 +1344,7 @@ var BrowserPopupRenderer = class {
1327
1344
  document.body.appendChild(this.container);
1328
1345
  }
1329
1346
  }
1330
- show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId) {
1347
+ show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId, _renderChrome) {
1331
1348
  if (this.visible) return;
1332
1349
  if (!this.container || !document.body.contains(this.container)) this.init();
1333
1350
  if (!this.container) return;
@@ -2188,6 +2205,8 @@ var DeepdotsPopups = class {
2188
2205
  this.deferredExitTimers = [];
2189
2206
  this.baseUrl = "";
2190
2207
  this.env = "production";
2208
+ /** Si el SDK pinta el "modal" del popup (RN). Default true; false = el host lo envuelve. */
2209
+ this.renderChrome = true;
2191
2210
  /** Identidad + sesión (Fase 1 tracking). Null hasta init(). */
2192
2211
  this.tracking = null;
2193
2212
  /** Capa de analytics (canal separado del feedback). Null hasta init(). */
@@ -2238,6 +2257,7 @@ var DeepdotsPopups = class {
2238
2257
  const env = resolveEnvironment(config.nodeEnv);
2239
2258
  this.baseUrl = env.apiBaseUrl;
2240
2259
  this.env = config.nodeEnv || "production";
2260
+ this.renderChrome = config.renderChrome ?? true;
2241
2261
  this.config = {
2242
2262
  apiKey: config.apiKey || void 0,
2243
2263
  debug: config.debug || false,
@@ -2423,6 +2443,22 @@ var DeepdotsPopups = class {
2423
2443
  trackFunnelStep(funnel, step, taskId, params) {
2424
2444
  this.track("deepdots_funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
2425
2445
  }
2446
+ /**
2447
+ * Product Effectiveness: una interacción significativa del usuario (`interaction_type` es la
2448
+ * dimensión de agrupación, p. ej. `get_help`, `homepage`, `contact_support`).
2449
+ *
2450
+ * Existe como evento RESERVADO para que la convención no la invente cada host: un `track()`
2451
+ * custom acaba como `deepdots_event_…` y no lo cuenta la analítica de effectiveness.
2452
+ * Un `interactionType` vacío se descarta con warning (no aporta dato agrupable).
2453
+ */
2454
+ trackMeaningfulInteraction(interactionType, params) {
2455
+ const type = typeof interactionType === "string" ? interactionType.trim() : "";
2456
+ if (!type) {
2457
+ console.warn("[DeepdotsPopups] trackMeaningfulInteraction discarded: empty interaction_type");
2458
+ return;
2459
+ }
2460
+ this.track("deepdots_meaningful_interaction", { interaction_type: type, ...params ?? {} });
2461
+ }
2426
2462
  /**
2427
2463
  * Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app).
2428
2464
  * No-op si tracking off. El evento se descarta (con warning) si el `channel` no es válido,
@@ -3023,7 +3059,8 @@ var DeepdotsPopups = class {
3023
3059
  style,
3024
3060
  sessionId,
3025
3061
  miniService,
3026
- analyticsFeedbackSessionId
3062
+ analyticsFeedbackSessionId,
3063
+ this.renderChrome
3027
3064
  );
3028
3065
  return;
3029
3066
  }