@magicfeedback/popup-sdk 1.7.1 → 1.7.2

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.
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  LABELS,
3
+ PRIMARY_COLOR_JS,
3
4
  RESOLVE_LOCALE_JS,
5
+ REVEAL_TIMEOUT_MS,
4
6
  RTL_LOCALES,
5
7
  STORAGE_KEYS,
6
8
  TrackingManager,
@@ -15,7 +17,7 @@ import {
15
17
  sdkWarn,
16
18
  setLogger,
17
19
  style_default
18
- } from "./chunk-WNIUIOKF.mjs";
20
+ } from "./chunk-322DDMX6.mjs";
19
21
 
20
22
  // src/react-native/index.tsx
21
23
  import { createContext, useContext, useEffect, useRef, useState } from "react";
@@ -211,6 +213,30 @@ function resolveEnvironment(nodeEnv) {
211
213
  }
212
214
 
213
215
  // src/ui/surveyHtml.ts
216
+ var REVEAL_JS = `
217
+ function ddCreateReveal(popup, onReveal, timeoutMs){
218
+ var revealed=false, timer=null;
219
+ function reveal(){
220
+ if(revealed){ return; }
221
+ revealed=true;
222
+ if(timer){ clearTimeout(timer); timer=null; }
223
+ onReveal();
224
+ }
225
+ function revealWhenPainted(){
226
+ if(revealed){ return; }
227
+ var imgs=popup.querySelectorAll('img'), pending=[];
228
+ for(var i=0;i<imgs.length;i++){ if(!imgs[i].complete){ pending.push(imgs[i]); } }
229
+ if(!pending.length){ reveal(); return; }
230
+ var left=pending.length;
231
+ function onSettled(){ left--; if(left<=0){ reveal(); } }
232
+ for(var j=0;j<pending.length;j++){
233
+ pending[j].addEventListener('load', onSettled);
234
+ pending[j].addEventListener('error', onSettled);
235
+ }
236
+ }
237
+ timer=setTimeout(reveal, timeoutMs);
238
+ return { reveal: reveal, whenPainted: revealWhenPainted };
239
+ }`;
214
240
  var POSITION_MAP = {
215
241
  center: { justifyContent: "center", alignItems: "center", padding: "0", background: "rgba(0,0,0,0.5)" },
216
242
  bottom: { justifyContent: "center", alignItems: "flex-end", padding: "16px", background: "transparent" },
@@ -284,7 +310,13 @@ function buildSurveyHtml(opts) {
284
310
  ${style_default}
285
311
  ${fontFaceCss}
286
312
  html,body{margin:0;padding:0;height:100%;font-family:${fontFamilyValue}}
287
- body{display:${bodyDisplay};justify-content:${pos.justifyContent};align-items:${pos.alignItems};background:${bodyBg};padding:${bodyPadding};box-sizing:border-box}
313
+ /* Apertura diferida: ni la tarjeta ni el velo se pintan hasta que el survey est\xE1 listo
314
+ (\`dd-ready\`). Si el fondo entrara antes, en RN se ver\xEDa el velo oscuro varios cientos de ms
315
+ antes que el contenido, que es la misma mala sensaci\xF3n que el spinner. */
316
+ body{display:${bodyDisplay};justify-content:${pos.justifyContent};align-items:${pos.alignItems};background:transparent;padding:${bodyPadding};box-sizing:border-box}
317
+ body.dd-ready{background:${bodyBg}}
318
+ #dd-popup{visibility:hidden}
319
+ body.dd-ready #dd-popup{visibility:visible}
288
320
  .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};height:${cardHeight};max-height:${cardMaxHeight};box-sizing:border-box}
289
321
  .deepdots-popup-header{display:flex;justify-content:space-between;align-items:center;gap:12px;width:100%;flex:0 0 auto}
290
322
  /* Neutraliza la regla \`.deepdots-popup h2\` del CSS del survey (uppercase, centrado y
@@ -318,6 +350,13 @@ body{display:${bodyDisplay};justify-content:${pos.justifyContent};align-items:${
318
350
  #dd-form-wrapper{width:100%;flex:1 1 auto}
319
351
  #mf{width:100%;box-sizing:border-box;visibility:hidden}
320
352
  .deepdots-popup-main *{max-width:100%;box-sizing:border-box}
353
+ /* El margen lateral lo pone la tarjeta: el CSS del survey lo a\xF1ade otra vez en el contenedor, en
354
+ el formulario y en el bloque de cada pregunta, y eso deja el enunciado y las opciones m\xE1s
355
+ adentro que el logo, el t\xEDtulo y la barra de progreso. Solo el lateral; el vertical separa las
356
+ preguntas. Mismas reglas que en el popup DOM (renderPopup.ts). */
357
+ .deepdots-popup .magicfeedback-container,
358
+ .deepdots-popup .magicfeedback-form,
359
+ .deepdots-popup .magicfeedback-div{padding-left:0;padding-right:0}
321
360
  #dd-logo{max-height:40px;max-width:100%;object-fit:contain;display:block;margin:12px 0 0 0}
322
361
  .deepdots-error-hint{display:none;margin:12px 0 0 0;padding:10px 12px;border-radius:6px;background:#FEF3C7;color:#92400E;border:1px solid #FCD34D;font-size:13px}
323
362
  /* Pantalla final: el HTML del editor de la plataforma (imagen + texto), centrado. */
@@ -325,7 +364,7 @@ body{display:${bodyDisplay};justify-content:${pos.justifyContent};align-items:${
325
364
  .deepdots-success img{max-width:100%;height:auto;margin:0 auto 16px auto;display:block}
326
365
  .deepdots-success p{margin:0;font-size:16px;font-weight:600;line-height:1.4}
327
366
  .deepdots-popup-footer{display:flex;flex-direction:row-reverse;justify-content:space-between;align-items:center;gap:4px;margin-top:auto;width:100%;padding-top:12px}
328
- .dd-nav-btn{display:none;border:none;min-height:44px;padding:12px 24px;border-radius:6px;cursor:pointer;font-size:15px;font-weight:600;align-items:center;justify-content:center;text-align:center}
367
+ .dd-nav-btn{display:none;border:none;min-height:44px;padding:12px 24px;border-radius:999px;cursor:pointer;font-size:15px;font-weight:600;align-items:center;justify-content:center;text-align:center}
329
368
  /* Secundario como bot\xF3n de texto: sin borde ni fondo, para que el primario sea la \xFAnica CTA. */
330
369
  #dd-back{background:transparent;color:${textMuted};border:none}
331
370
  #dd-start,#dd-complete,#dd-submit{background:#1E293B;color:#fff;border:none}
@@ -550,9 +589,23 @@ ${customCss}
550
589
  }
551
590
  }
552
591
 
592
+ ${PRIMARY_COLOR_JS}
593
+ ${REVEAL_JS}
594
+ // El popup se ense\xF1a cuando el survey est\xE1 pintado; \`ready\` deja al host abrir su propio
595
+ // Modal en ese momento en vez de montar un WebView en blanco.
596
+ var ddReveal=ddCreateReveal(popup, function(){
597
+ document.body.classList.add('dd-ready');
598
+ emitJSON('ready');
599
+ }, ${REVEAL_TIMEOUT_MS});
600
+
553
601
  function setLoading(isLoading){
554
602
  spinner.style.display=isLoading?'flex':'none';
555
- if(!isLoading){ formHost.style.visibility='visible'; }
603
+ if(!isLoading){
604
+ formHost.style.visibility='visible';
605
+ // Fin de la carga, sea por survey cargado o por error. Entre p\xE1ginas es no-op: la
606
+ // revelaci\xF3n solo act\xFAa la primera vez.
607
+ ddReveal.whenPainted();
608
+ }
556
609
  footer.style.display=isLoading?'none':'flex';
557
610
  backBtn.disabled=isLoading; startBtn.disabled=isLoading; completeBtn.disabled=isLoading; submitBtn.disabled=isLoading;
558
611
  if(isLoading){ updateButtons(null); }
@@ -595,6 +648,10 @@ ${customCss}
595
648
  if(s.loadingBarColor){ progressBar.style.background=s.loadingBarColor; progressFollowUp.style.background=s.loadingBarColor; }
596
649
  if(chromeOn && s.boxBackgroundColor){ popup.style.background=s.boxBackgroundColor; }
597
650
  if(s.contentAlign){ main.style.justifyContent = s.contentAlign==='center' ? 'center' : 'flex-start'; }
651
+ // El survey pinta sus controles con --mf-primary, que sale de primaryColor; si la
652
+ // integraci\xF3n solo configura el del bot\xF3n, el popup mezclar\xEDa el color de la marca
653
+ // con el gris azulado por defecto del paquete.
654
+ ddApplySurveyPrimaryColor(popup, s);
598
655
  if(s.buttonPrimaryColor){
599
656
  submitBtn.style.background=s.buttonPrimaryColor; submitBtn.style.border='none'; submitBtn.style.color='#fff';
600
657
  startBtn.style.background=s.buttonPrimaryColor; startBtn.style.border='none'; startBtn.style.color='#fff';
@@ -686,6 +743,7 @@ ${customCss}
686
743
  }
687
744
 
688
745
  // src/platform/react-native-renderer.ts
746
+ var NATIVE_READY_GRACE_MS = 500;
689
747
  var ReactNativePopupRenderer = class {
690
748
  constructor(options = {}) {
691
749
  this.options = options;
@@ -693,6 +751,8 @@ var ReactNativePopupRenderer = class {
693
751
  this.onCloseFn = null;
694
752
  this.currentSurveyId = null;
695
753
  this.partialEmitted = false;
754
+ this.readyEmitted = false;
755
+ this.readyTimer = null;
696
756
  }
697
757
  init() {
698
758
  }
@@ -701,6 +761,8 @@ var ReactNativePopupRenderer = class {
701
761
  this.onCloseFn = onClose;
702
762
  this.currentSurveyId = surveyId;
703
763
  this.partialEmitted = false;
764
+ this.readyEmitted = false;
765
+ this.armReadyFallback(surveyId);
704
766
  const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null, analyticsFeedbackSessionId ?? null);
705
767
  const html = buildSurveyHtml({
706
768
  surveyId,
@@ -728,9 +790,32 @@ var ReactNativePopupRenderer = class {
728
790
  }
729
791
  }
730
792
  hide() {
793
+ this.clearReadyFallback();
731
794
  this.options.onHide?.();
732
795
  this.currentSurveyId = null;
733
796
  }
797
+ /** Respaldo del aviso de listo por si el WebView no llega a ejecutar el HTML. */
798
+ armReadyFallback(surveyId) {
799
+ this.clearReadyFallback();
800
+ if (!this.options.onReady) return;
801
+ this.readyTimer = setTimeout(() => {
802
+ this.readyTimer = null;
803
+ this.emitReady(surveyId);
804
+ }, REVEAL_TIMEOUT_MS + NATIVE_READY_GRACE_MS);
805
+ }
806
+ clearReadyFallback() {
807
+ if (this.readyTimer !== null) {
808
+ clearTimeout(this.readyTimer);
809
+ this.readyTimer = null;
810
+ }
811
+ }
812
+ /** Idempotente: el WebView puede avisar a la vez que vence el respaldo. */
813
+ emitReady(surveyId) {
814
+ if (this.readyEmitted) return;
815
+ this.readyEmitted = true;
816
+ this.clearReadyFallback();
817
+ this.options.onReady?.({ surveyId });
818
+ }
734
819
  /**
735
820
  * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
736
821
  * eventos del SDK: primera interacción → `popup_clicked` (PARTIAL), completado →
@@ -746,6 +831,9 @@ var ReactNativePopupRenderer = class {
746
831
  name = raw;
747
832
  }
748
833
  switch (name) {
834
+ case "ready":
835
+ this.emitReady(surveyId);
836
+ break;
749
837
  case "loaded":
750
838
  case "before_submit":
751
839
  case "after_submit":
@@ -773,6 +861,33 @@ function createReactNativeRenderer() {
773
861
  return new ReactNativePopupRenderer();
774
862
  }
775
863
 
864
+ // src/ui/preload.ts
865
+ var defaultLoader = () => import("./renderPopup-SNDPX5RX.mjs");
866
+ var pending = null;
867
+ function preloadRenderPopup(loader = defaultLoader) {
868
+ if (pending) return pending;
869
+ try {
870
+ pending = loader().then(() => void 0).catch(() => {
871
+ pending = null;
872
+ });
873
+ } catch {
874
+ pending = null;
875
+ return Promise.resolve();
876
+ }
877
+ return pending;
878
+ }
879
+ function schedulePreloadRenderPopup(loader) {
880
+ const run = () => {
881
+ void preloadRenderPopup(loader);
882
+ };
883
+ const idle = globalThis.requestIdleCallback;
884
+ if (typeof idle === "function") {
885
+ idle(run, { timeout: 2e3 });
886
+ return;
887
+ }
888
+ setTimeout(run, 0);
889
+ }
890
+
776
891
  // src/platform/renderer.ts
777
892
  var NoopPopupRenderer = class {
778
893
  show() {
@@ -807,13 +922,20 @@ var BrowserPopupRenderer = class {
807
922
  document.body.appendChild(this.container);
808
923
  }
809
924
  }
925
+ /**
926
+ * Descarga el chunk de `renderPopup` antes de que ningún trigger dispare, para que abrir el
927
+ * popup no empiece por esperar ~238 KB de JS.
928
+ */
929
+ preload() {
930
+ schedulePreloadRenderPopup();
931
+ }
810
932
  show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId, _renderChrome, options) {
811
933
  if (this.visible) return;
812
934
  if (!this.container || !document.body.contains(this.container)) this.init();
813
935
  if (!this.container) return;
814
936
  this.visible = true;
815
937
  const container = this.container;
816
- void import("./renderPopup-EWGKG53W.mjs").then(({ renderPopup }) => {
938
+ void import("./renderPopup-SNDPX5RX.mjs").then(({ renderPopup }) => {
817
939
  renderPopup(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService, analyticsFeedbackSessionId, options);
818
940
  });
819
941
  }
@@ -821,6 +943,8 @@ var BrowserPopupRenderer = class {
821
943
  this.visible = false;
822
944
  if (this.container) {
823
945
  this.container.style.display = "none";
946
+ this.container.style.visibility = "";
947
+ this.container.style.pointerEvents = "";
824
948
  this.container.innerHTML = "";
825
949
  }
826
950
  }
@@ -1890,6 +2014,7 @@ var DeepdotsPopups = class {
1890
2014
  this.popupDefinitions = this.validatePopupDefinitions(defs);
1891
2015
  this.popupsLoaded = true;
1892
2016
  this.log("Popups loaded from API", this.popupDefinitions);
2017
+ if (this.popupDefinitions.length) this.renderer.preload?.();
1893
2018
  this.configureTriggersFromDefinitions();
1894
2019
  this.processDeferredExitQueue();
1895
2020
  if (this.pendingAutoLaunch) {
@@ -2377,7 +2502,7 @@ var DeepdotsPopups = class {
2377
2502
  processDeferredExitQueue() {
2378
2503
  const queue = this.getDeferredExitQueue();
2379
2504
  if (!queue.length) return;
2380
- const pending = [];
2505
+ const pending2 = [];
2381
2506
  const now = Date.now();
2382
2507
  queue.forEach((item) => {
2383
2508
  const def = this.popupDefinitions.find((popup) => popup.id === item.id && popup.surveyId === item.surveyId);
@@ -2388,10 +2513,10 @@ var DeepdotsPopups = class {
2388
2513
  this.tryShowDeferredExitPopup(item);
2389
2514
  return;
2390
2515
  }
2391
- pending.push(item);
2516
+ pending2.push(item);
2392
2517
  this.scheduleDeferredExitPopup(item);
2393
2518
  });
2394
- this.setDeferredExitQueue(pending);
2519
+ this.setDeferredExitQueue(pending2);
2395
2520
  }
2396
2521
  scheduleDeferredExitPopup(item) {
2397
2522
  if (typeof window === "undefined") return;
@@ -2464,7 +2589,7 @@ var DeepdotsPopups = class {
2464
2589
  async fetchPopupsFromServer() {
2465
2590
  const apiKey = this.config?.apiKey;
2466
2591
  const baseUrl = this.baseUrl;
2467
- const userId = this.config?.userId;
2592
+ const userId = this.tracking?.getUserId() ?? this.config?.userId;
2468
2593
  if (!apiKey || !baseUrl) {
2469
2594
  this.log("Missing apiKey or baseUrl. Skipping popups fetch.");
2470
2595
  return [];
@@ -2608,7 +2733,7 @@ var DeepdotsPopups = class {
2608
2733
  }
2609
2734
  if (!this.popupContainer) return;
2610
2735
  const container = this.popupContainer;
2611
- void import("./renderPopup-EWGKG53W.mjs").then(({ renderPopup }) => {
2736
+ void import("./renderPopup-SNDPX5RX.mjs").then(({ renderPopup }) => {
2612
2737
  renderPopup(
2613
2738
  container,
2614
2739
  surveyId,
@@ -2889,16 +3014,24 @@ function DeepdotsProvider({ config, children }) {
2889
3014
  }, []);
2890
3015
  return /* @__PURE__ */ jsxs(DeepdotsContext.Provider, { value: sdk, children: [
2891
3016
  children,
2892
- survey ? /* @__PURE__ */ jsx(Modal, { visible: true, transparent: true, animationType: "slide", children: /* @__PURE__ */ jsx(View, { style: { flex: 1 }, children: /* @__PURE__ */ jsx(
2893
- WebView,
2894
- {
2895
- style: { flex: 1 },
2896
- originWhitelist: ["*"],
2897
- javaScriptEnabled: true,
2898
- source: { html: survey.html, baseUrl: "https://sdk.deepdots.com/" },
2899
- onMessage: (e) => rendererRef.current?.handleMessage(e.nativeEvent.data)
2900
- }
2901
- ) }) }) : null
3017
+ survey ? (
3018
+ // El Modal se abre en cuanto hay popup, y es el HTML del WebView el que se revela solo
3019
+ // cuando el survey está pintado (apertura diferida): con el Modal transparente, hasta ese
3020
+ // momento no se ve nada, ni velo ni spinner. No se difiere el Modal con `onReady` porque
3021
+ // React Native no monta los hijos de un Modal cerrado: el WebView no llegaría a cargar y
3022
+ // el aviso no llegaría nunca. El host que quiera diferir su contenedor debe montar el
3023
+ // WebView invisible (ver el ejemplo de `ReactNativePopupRenderer`).
3024
+ /* @__PURE__ */ jsx(Modal, { visible: true, transparent: true, animationType: "slide", children: /* @__PURE__ */ jsx(View, { style: { flex: 1 }, children: /* @__PURE__ */ jsx(
3025
+ WebView,
3026
+ {
3027
+ style: { flex: 1 },
3028
+ originWhitelist: ["*"],
3029
+ javaScriptEnabled: true,
3030
+ source: { html: survey.html, baseUrl: "https://sdk.deepdots.com/" },
3031
+ onMessage: (e) => rendererRef.current?.handleMessage(e.nativeEvent.data)
3032
+ }
3033
+ ) }) })
3034
+ ) : null
2902
3035
  ] });
2903
3036
  }
2904
3037
  export {