@magicfeedback/popup-sdk 1.14.0 → 1.15.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
@@ -143,6 +143,11 @@ Config fields:
143
143
  or positioning, so the host mounts its own `Modal`/container and controls the styling. This
144
144
  avoids a "double modal" when the host already wraps the WebView. The survey stays fully
145
145
  functional (message bridge, form, nav buttons, close). No effect on the web DOM popup.
146
+ - `cspNonce?: string` — for pages with a strict Content-Security-Policy (`style-src 'nonce-…'`
147
+ without `'unsafe-inline'`). The popup puts it on every `<style>` it injects into `<head>`
148
+ (survey stylesheet, spinner, responsive rules, `surveyCss`, `@font-face`); without it the
149
+ browser blocks them and the survey renders unstyled. Pass the same nonce your CSP header
150
+ uses for that response (in Angular, the `CSP_NONCE` token). Web only.
146
151
  - `language?: string` — BCP-47 tag (`'en'`, `'en-US'`, …) used for popup language
147
152
  segmentation (`segments.lang`) and for the analytics context. If omitted it is
148
153
  auto-detected from `navigator.language`, falling back to the `Intl` locale (which is
@@ -534,7 +539,7 @@ interface DeepdotsEvent {
534
539
  - The default browser renderer uses Deepdots forms and applies button labels from `actions.accept`, `actions.start`, `actions.back`, and `actions.complete`.
535
540
  - `title`, `message`, and `style` belong to the popup definition contract, but the current browser renderer focuses on the embedded form and does not render those fields as standalone popup copy/layout controls.
536
541
  - `actions.decline` is accepted in the definition shape, but the current browser renderer does not render a dedicated decline button or enforce `decline.cooldownDays`.
537
- - In browser mode, the SDK creates `#deepdots-popup-container` and injects the shared popup stylesheet from jsDelivr.
542
+ - In browser mode, the default renderer creates a single `#deepdots-popup-container` on `document.body` and injects the popup and survey stylesheets into `<head>` (bundled with the SDK, no external request). With a custom renderer (`setRenderer`) the SDK adds no container of its own.
538
543
 
539
544
  ## Running The Examples Locally
540
545
 
package/dist/index.d.mts CHANGED
@@ -332,6 +332,15 @@ interface DeepdotsInitParams {
332
332
  * en vez de cambiar los defaults que comparten todos los clientes.
333
333
  */
334
334
  surveyCss?: string;
335
+ /**
336
+ * Nonce de la Content-Security-Policy de la página, para hosts con una CSP estricta
337
+ * (`style-src 'nonce-…'` sin `'unsafe-inline'`). El popup lo pone en cada `<style>` que
338
+ * inyecta en el `<head>` (hoja del survey, spinner, responsive, `surveyCss`, `@font-face`);
339
+ * sin él el navegador las bloquea y el survey sale sin estilos. Debe ser el MISMO nonce de
340
+ * la cabecera CSP de esa respuesta (en Angular, el token `CSP_NONCE`). Solo web: en React
341
+ * Native el survey vive en su propio WebView y no le afecta la CSP del host.
342
+ */
343
+ cspNonce?: string;
335
344
  }
336
345
  interface DeepdotsConfig {
337
346
  /** API key for authentication */
@@ -508,6 +517,12 @@ interface PopupRenderOptions {
508
517
  * (`formData.lang[0]`) en cuanto este carga, y ambos los pisa `actions.*.label` de la API.
509
518
  */
510
519
  language?: string;
520
+ /**
521
+ * Nonce de la Content-Security-Policy del host (`DeepdotsInitParams.cspNonce`). Se pone en
522
+ * cada `<style>` que inyecta el popup DOM; sin él, una CSP con `style-src 'nonce-…'` bloquea
523
+ * la hoja del survey. No aplica a React Native (el survey vive en su propio WebView).
524
+ */
525
+ cspNonce?: string;
511
526
  }
512
527
  interface PopupRenderer {
513
528
  /** Preparar recursos si aplica */
@@ -727,7 +742,6 @@ declare class DeepdotsPopups {
727
742
  private renderer;
728
743
  /** Logger para el output de debug. Default `console`; el host puede inyectar el suyo en init(). */
729
744
  private logger;
730
- private popupContainer;
731
745
  private popupDefinitions;
732
746
  private popupsLoaded;
733
747
  /**
@@ -752,6 +766,7 @@ declare class DeepdotsPopups {
752
766
  private showProgressBar;
753
767
  /** CSS del host inyectado al final de los estilos del popup. */
754
768
  private surveyCss;
769
+ private cspNonce;
755
770
  /** Identidad + sesión (Fase 1 tracking). Null hasta init(). */
756
771
  private tracking;
757
772
  /** Capa de analytics (canal separado del feedback). Null hasta init(). */
@@ -1064,8 +1079,6 @@ declare class DeepdotsPopups {
1064
1079
  on(eventType: DeepdotsEventType, listener: EventListener): void;
1065
1080
  /** Remove an event listener */
1066
1081
  off(eventType: DeepdotsEventType, listener: EventListener): void;
1067
- /** Setup popup container element */
1068
- private setupPopupContainer;
1069
1082
  /** Render the popup UI */
1070
1083
  private renderPopup;
1071
1084
  /** Hide the popup */
package/dist/index.d.ts CHANGED
@@ -332,6 +332,15 @@ interface DeepdotsInitParams {
332
332
  * en vez de cambiar los defaults que comparten todos los clientes.
333
333
  */
334
334
  surveyCss?: string;
335
+ /**
336
+ * Nonce de la Content-Security-Policy de la página, para hosts con una CSP estricta
337
+ * (`style-src 'nonce-…'` sin `'unsafe-inline'`). El popup lo pone en cada `<style>` que
338
+ * inyecta en el `<head>` (hoja del survey, spinner, responsive, `surveyCss`, `@font-face`);
339
+ * sin él el navegador las bloquea y el survey sale sin estilos. Debe ser el MISMO nonce de
340
+ * la cabecera CSP de esa respuesta (en Angular, el token `CSP_NONCE`). Solo web: en React
341
+ * Native el survey vive en su propio WebView y no le afecta la CSP del host.
342
+ */
343
+ cspNonce?: string;
335
344
  }
336
345
  interface DeepdotsConfig {
337
346
  /** API key for authentication */
@@ -508,6 +517,12 @@ interface PopupRenderOptions {
508
517
  * (`formData.lang[0]`) en cuanto este carga, y ambos los pisa `actions.*.label` de la API.
509
518
  */
510
519
  language?: string;
520
+ /**
521
+ * Nonce de la Content-Security-Policy del host (`DeepdotsInitParams.cspNonce`). Se pone en
522
+ * cada `<style>` que inyecta el popup DOM; sin él, una CSP con `style-src 'nonce-…'` bloquea
523
+ * la hoja del survey. No aplica a React Native (el survey vive en su propio WebView).
524
+ */
525
+ cspNonce?: string;
511
526
  }
512
527
  interface PopupRenderer {
513
528
  /** Preparar recursos si aplica */
@@ -727,7 +742,6 @@ declare class DeepdotsPopups {
727
742
  private renderer;
728
743
  /** Logger para el output de debug. Default `console`; el host puede inyectar el suyo en init(). */
729
744
  private logger;
730
- private popupContainer;
731
745
  private popupDefinitions;
732
746
  private popupsLoaded;
733
747
  /**
@@ -752,6 +766,7 @@ declare class DeepdotsPopups {
752
766
  private showProgressBar;
753
767
  /** CSS del host inyectado al final de los estilos del popup. */
754
768
  private surveyCss;
769
+ private cspNonce;
755
770
  /** Identidad + sesión (Fase 1 tracking). Null hasta init(). */
756
771
  private tracking;
757
772
  /** Capa de analytics (canal separado del feedback). Null hasta init(). */
@@ -1064,8 +1079,6 @@ declare class DeepdotsPopups {
1064
1079
  on(eventType: DeepdotsEventType, listener: EventListener): void;
1065
1080
  /** Remove an event listener */
1066
1081
  off(eventType: DeepdotsEventType, listener: EventListener): void;
1067
- /** Setup popup container element */
1068
- private setupPopupContainer;
1069
1082
  /** Render the popup UI */
1070
1083
  private renderPopup;
1071
1084
  /** Hide the popup */
package/dist/index.js CHANGED
@@ -5628,30 +5628,35 @@ __export(renderPopup_exports, {
5628
5628
  SURVEY_ALIGNMENT_STYLE_ID: () => SURVEY_ALIGNMENT_STYLE_ID,
5629
5629
  renderPopup: () => renderPopup
5630
5630
  });
5631
- function ensureMagicFeedbackStyles(_popup) {
5632
- const STYLE_ID = "magicfeedback-sdk-styles";
5633
- if (document.getElementById(STYLE_ID)) return;
5634
- const style = document.createElement("style");
5635
- style.id = STYLE_ID;
5636
- style.textContent = style_default;
5637
- document.head.appendChild(style);
5638
- }
5639
- function ensureSurveyAlignmentStyles() {
5640
- if (document.getElementById(SURVEY_ALIGNMENT_STYLE_ID)) return;
5641
- const style = document.createElement("style");
5642
- style.id = SURVEY_ALIGNMENT_STYLE_ID;
5643
- style.textContent = `
5631
+ function ensureStyle(id, css, nonce) {
5632
+ let el = document.getElementById(id);
5633
+ if (el && nonce && el.getAttribute("nonce") !== nonce) {
5634
+ el.remove();
5635
+ el = null;
5636
+ }
5637
+ if (!el) {
5638
+ el = document.createElement("style");
5639
+ el.id = id;
5640
+ if (nonce) el.setAttribute("nonce", nonce);
5641
+ el.textContent = css;
5642
+ document.head.appendChild(el);
5643
+ } else if (el.textContent !== css) {
5644
+ el.textContent = css;
5645
+ }
5646
+ return el;
5647
+ }
5648
+ function ensureMagicFeedbackStyles(nonce) {
5649
+ ensureStyle("magicfeedback-sdk-styles", style_default, nonce);
5650
+ }
5651
+ function ensureSurveyAlignmentStyles(nonce) {
5652
+ ensureStyle(SURVEY_ALIGNMENT_STYLE_ID, `
5644
5653
  .deepdots-popup .magicfeedback-container,
5645
5654
  .deepdots-popup .magicfeedback-form,
5646
5655
  .deepdots-popup .magicfeedback-div { padding-left: 0; padding-right: 0; }
5647
- `;
5648
- document.head.appendChild(style);
5649
- }
5650
- function ensureSpinnerStyles(_popup) {
5651
- if (document.getElementById("deepdots-spinner-styles")) return;
5652
- const style = document.createElement("style");
5653
- style.id = "deepdots-spinner-styles";
5654
- style.textContent = `
5656
+ `, nonce);
5657
+ }
5658
+ function ensureSpinnerStyles(nonce) {
5659
+ ensureStyle("deepdots-spinner-styles", `
5655
5660
  /* El enunciado trae margin-top:20px del CSS de surveys, que sumaba al hueco de la barra
5656
5661
  de progreso y descompensaba la simetr\xEDa. Solo se anula en el primero: entre preguntas
5657
5662
  de la misma p\xE1gina sigue separando. */
@@ -5659,14 +5664,10 @@ function ensureSpinnerStyles(_popup) {
5659
5664
  @keyframes ddspin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
5660
5665
  .mf-spinner { display:flex; justify-content:center; align-items:center; padding:8px 0; }
5661
5666
  .mf-spinner-circle { width:28px; height:28px; border:3px solid #e0e6ed; border-top-color:#1E293B; border-radius:50%; animation: ddspin 0.9s linear infinite; }
5662
- `;
5663
- document.head.appendChild(style);
5664
- }
5665
- function ensureResponsiveStyles(_popup) {
5666
- if (document.getElementById("deepdots-responsive-styles")) return;
5667
- const style = document.createElement("style");
5668
- style.id = "deepdots-responsive-styles";
5669
- style.textContent = `
5667
+ `, nonce);
5668
+ }
5669
+ function ensureResponsiveStyles(nonce) {
5670
+ ensureStyle("deepdots-responsive-styles", `
5670
5671
  /* Responsive adjustments */
5671
5672
  @media (max-width: 640px) {
5672
5673
  .deepdots-popup {
@@ -5699,32 +5700,20 @@ function ensureResponsiveStyles(_popup) {
5699
5700
  overflow-y: auto !important;
5700
5701
  }
5701
5702
  }
5702
- `;
5703
- document.head.appendChild(style);
5703
+ `, nonce);
5704
5704
  }
5705
- function ensureCustomCss(css) {
5705
+ function ensureCustomCss(css, nonce) {
5706
5706
  const STYLE_ID = "deepdots-custom-css";
5707
- const existing = document.getElementById(STYLE_ID);
5708
5707
  if (!css) {
5709
- existing?.remove();
5708
+ document.getElementById(STYLE_ID)?.remove();
5710
5709
  return;
5711
5710
  }
5712
- const el = existing ?? document.createElement("style");
5713
- el.id = STYLE_ID;
5714
- if (el.textContent !== css) el.textContent = css;
5715
- document.head.appendChild(el);
5711
+ document.head.appendChild(ensureStyle(STYLE_ID, css, nonce));
5716
5712
  }
5717
- function ensureFontFace(family, url) {
5713
+ function ensureFontFace(family, url, nonce) {
5718
5714
  const css = buildFontFaceCss(family, url);
5719
5715
  if (!css) return;
5720
- const STYLE_ID = "deepdots-font-face";
5721
- let el = document.getElementById(STYLE_ID);
5722
- if (!el) {
5723
- el = document.createElement("style");
5724
- el.id = STYLE_ID;
5725
- document.head.appendChild(el);
5726
- }
5727
- if (el.textContent !== css) el.textContent = css;
5716
+ ensureStyle("deepdots-font-face", css, nonce);
5728
5717
  }
5729
5718
  function hasFinePointer() {
5730
5719
  return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(pointer: fine)").matches;
@@ -5734,6 +5723,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
5734
5723
  let stylesInjected = false;
5735
5724
  let pageDepth = 0;
5736
5725
  let onStartPage = false;
5726
+ const cspNonce = options?.cspNonce?.trim() || void 0;
5737
5727
  let labels = getLabels(options?.language);
5738
5728
  let actionLabels = resolveActionLabels(actions, options?.language);
5739
5729
  let direction = directionFor(options?.language);
@@ -5785,7 +5775,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
5785
5775
  popup.style.fontFamily = familyValue;
5786
5776
  popup.style.setProperty("--deepdots-font", familyValue);
5787
5777
  popup.classList.add("deepdots-has-font");
5788
- if (font.url) ensureFontFace(font.family, font.url);
5778
+ if (font.url) ensureFontFace(font.family, font.url, cspNonce);
5789
5779
  }
5790
5780
  const header = document.createElement("div");
5791
5781
  header.className = "deepdots-popup-header";
@@ -5901,11 +5891,11 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
5901
5891
  }
5902
5892
  progressFollowUp.style.display = p.followup ? "inline-block" : "none";
5903
5893
  }
5904
- ensureMagicFeedbackStyles(popup);
5905
- ensureSurveyAlignmentStyles();
5906
- ensureSpinnerStyles(popup);
5907
- ensureResponsiveStyles(popup);
5908
- ensureCustomCss(options?.surveyCss);
5894
+ ensureMagicFeedbackStyles(cspNonce);
5895
+ ensureSurveyAlignmentStyles(cspNonce);
5896
+ ensureSpinnerStyles(cspNonce);
5897
+ ensureResponsiveStyles(cspNonce);
5898
+ ensureCustomCss(options?.surveyCss, cspNonce);
5909
5899
  const containerContent = document.createElement("div");
5910
5900
  containerContent.id = "dd-content";
5911
5901
  containerContent.className = "deepdots-popup-container-content";
@@ -8393,8 +8383,6 @@ var DeepdotsPopups = class {
8393
8383
  this.renderer = createDefaultRenderer();
8394
8384
  /** Logger para el output de debug. Default `console`; el host puede inyectar el suyo en init(). */
8395
8385
  this.logger = console;
8396
- this.popupContainer = null;
8397
- // deprecated: mantenido para compatibilidad interna
8398
8386
  this.popupDefinitions = [];
8399
8387
  this.popupsLoaded = false;
8400
8388
  /**
@@ -8425,6 +8413,7 @@ var DeepdotsPopups = class {
8425
8413
  this.showProgressBar = void 0;
8426
8414
  /** CSS del host inyectado al final de los estilos del popup. */
8427
8415
  this.surveyCss = void 0;
8416
+ this.cspNonce = void 0;
8428
8417
  /** Identidad + sesión (Fase 1 tracking). Null hasta init(). */
8429
8418
  this.tracking = null;
8430
8419
  /** Capa de analytics (canal separado del feedback). Null hasta init(). */
@@ -8497,6 +8486,7 @@ var DeepdotsPopups = class {
8497
8486
  this.renderChrome = config.renderChrome ?? true;
8498
8487
  this.showProgressBar = config.showProgressBar;
8499
8488
  this.surveyCss = config.surveyCss;
8489
+ this.cspNonce = config.cspNonce;
8500
8490
  this.onFeedbackSession = config.onFeedbackSession;
8501
8491
  this.config = {
8502
8492
  apiKey: config.apiKey || void 0,
@@ -8597,7 +8587,6 @@ var DeepdotsPopups = class {
8597
8587
  this.initialized = true;
8598
8588
  this.log("SDK initialized", this.config);
8599
8589
  if (this.renderer.init) this.renderer.init();
8600
- this.setupPopupContainer();
8601
8590
  this.awaitingPopups = Boolean(this.config.apiKey && this.baseUrl);
8602
8591
  this.fetchPopupsFromServer().then((defs) => {
8603
8592
  if (!defs) this.dropPendingTriggers();
@@ -9568,27 +9557,6 @@ var DeepdotsPopups = class {
9568
9557
  listeners.delete(listener);
9569
9558
  }
9570
9559
  }
9571
- /** Setup popup container element */
9572
- setupPopupContainer() {
9573
- if (typeof document === "undefined") {
9574
- return;
9575
- }
9576
- this.popupContainer = document.createElement("div");
9577
- this.popupContainer.id = "deepdots-popup-container";
9578
- this.popupContainer.style.cssText = `
9579
- position: fixed;
9580
- top: 0;
9581
- left: 0;
9582
- width: 100%;
9583
- height: 100%;
9584
- display: none;
9585
- z-index: 999999;
9586
- background: rgba(0, 0, 0, 0.5);
9587
- justify-content: center;
9588
- align-items: center;
9589
- `;
9590
- document.body.appendChild(this.popupContainer);
9591
- }
9592
9560
  /** Render the popup UI */
9593
9561
  renderPopup(surveyId, productId, actions, style, title) {
9594
9562
  const userId = this.tracking?.getUserId() ?? this.config?.userId;
@@ -9596,54 +9564,25 @@ var DeepdotsPopups = class {
9596
9564
  const miniService = this.analytics?.getMiniService() ?? void 0;
9597
9565
  const analyticsFeedbackSessionId = this.analyticsFeedbackSessionId;
9598
9566
  this.log("tracking \xB7 survey identity \u2192", { userId: userId ?? null, sessionId: sessionId ?? null, miniService: miniService ?? null, analyticsFeedbackSessionId: analyticsFeedbackSessionId ?? null });
9599
- if (this.renderer) {
9600
- this.renderer.show(
9601
- surveyId,
9602
- productId,
9603
- actions,
9604
- (type, id, payload) => this.emitEvent(type, id, payload),
9605
- () => this.hidePopup(),
9606
- this.env,
9607
- userId,
9608
- style,
9609
- sessionId,
9610
- miniService,
9611
- analyticsFeedbackSessionId,
9612
- this.renderChrome,
9613
- { title, showProgressBar: this.showProgressBar, surveyCss: this.surveyCss, language: this.language }
9614
- );
9615
- return;
9616
- }
9617
- if (!this.popupContainer) return;
9618
- const container = this.popupContainer;
9619
- void Promise.resolve().then(() => (init_renderPopup(), renderPopup_exports)).then(({ renderPopup: renderPopup2 }) => {
9620
- renderPopup2(
9621
- container,
9622
- surveyId,
9623
- productId,
9624
- actions,
9625
- (type, id, payload) => this.emitEvent(type, id, payload),
9626
- () => this.hidePopup(),
9627
- this.env,
9628
- userId,
9629
- style,
9630
- sessionId,
9631
- miniService,
9632
- analyticsFeedbackSessionId,
9633
- { title, showProgressBar: this.showProgressBar, surveyCss: this.surveyCss, language: this.language }
9634
- );
9635
- });
9567
+ this.renderer.show(
9568
+ surveyId,
9569
+ productId,
9570
+ actions,
9571
+ (type, id, payload) => this.emitEvent(type, id, payload),
9572
+ () => this.hidePopup(),
9573
+ this.env,
9574
+ userId,
9575
+ style,
9576
+ sessionId,
9577
+ miniService,
9578
+ analyticsFeedbackSessionId,
9579
+ this.renderChrome,
9580
+ { title, showProgressBar: this.showProgressBar, surveyCss: this.surveyCss, language: this.language, cspNonce: this.cspNonce }
9581
+ );
9636
9582
  }
9637
9583
  /** Hide the popup */
9638
9584
  hidePopup() {
9639
- if (this.renderer) {
9640
- this.renderer.hide();
9641
- return;
9642
- }
9643
- if (this.popupContainer) {
9644
- this.popupContainer.style.display = "none";
9645
- this.popupContainer.innerHTML = "";
9646
- }
9585
+ this.renderer.hide();
9647
9586
  }
9648
9587
  /** Emit an event */
9649
9588
  emitEvent(type, surveyId, data) {