@magicfeedback/popup-sdk 1.8.0 → 1.8.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.
@@ -507,12 +507,14 @@ declare class DeepdotsPopups {
507
507
  * se llama en init(), al volver a foreground y al conceder el consentimiento.
508
508
  */
509
509
  private openSession;
510
+ /** Web: si la pestaña se ve. Sin `document` (RN) manda AppState vía onForeground/onBackground. */
511
+ private isDocumentVisible;
510
512
  /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
511
513
  previewAnalytics(): AnalyticsEnvelope;
512
514
  /**
513
515
  * Envía el lote acumulado de analytics y vacía el buffer.
514
- * `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
515
- * al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
516
+ * `final: true` (cierre de página/app): el lote no espera al primer POST en vuelo, porque
517
+ * el documento se muere; un lote que falle por red o 5xx se re-encola para el siguiente flush.
516
518
  */
517
519
  flushAnalytics(options?: {
518
520
  final?: boolean;
@@ -523,6 +525,7 @@ declare class DeepdotsPopups {
523
525
  private analyticsIdentity;
524
526
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
525
527
  private setupAnalyticsFlush;
528
+ private startAnalyticsFlushTimer;
526
529
  /** Enable auto-launch functionality with configured triggers */
527
530
  autoLaunch(): void;
528
531
  /** Inicia los triggers configurados */
@@ -602,11 +605,18 @@ declare class DeepdotsPopups {
602
605
  private mapPopupTriggerType;
603
606
  private markSurveyProgress;
604
607
  /**
605
- * Rehidrata el estado de redisplay desde el storage. `answeredSurveys` se reconstruye
606
- * desde las entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
608
+ * Fusiona el estado de redisplay del storage con el de memoria: por cada popup/survey gana
609
+ * la entrada MÁS RECIENTE. Fusionar y no sustituir es lo que hace convivir a varias
610
+ * pestañas sobre el mismo `localStorage`: se llama en el `init()`, antes de cada decisión
611
+ * de mostrar y antes de cada escritura. `answeredSurveys` se reconstruye desde las
612
+ * entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
607
613
  */
608
614
  private loadPopupState;
609
- /** Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria. */
615
+ /**
616
+ * Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria.
617
+ * Fusiona antes de escribir: escribir solo la foto en memoria borraba lo que otra pestaña
618
+ * hubiese guardado desde nuestro `init()`.
619
+ */
610
620
  private persistPopupState;
611
621
  private isPopupDefinition;
612
622
  private isPopupTrigger;
@@ -507,12 +507,14 @@ declare class DeepdotsPopups {
507
507
  * se llama en init(), al volver a foreground y al conceder el consentimiento.
508
508
  */
509
509
  private openSession;
510
+ /** Web: si la pestaña se ve. Sin `document` (RN) manda AppState vía onForeground/onBackground. */
511
+ private isDocumentVisible;
510
512
  /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
511
513
  previewAnalytics(): AnalyticsEnvelope;
512
514
  /**
513
515
  * Envía el lote acumulado de analytics y vacía el buffer.
514
- * `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
515
- * al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
516
+ * `final: true` (cierre de página/app): el lote no espera al primer POST en vuelo, porque
517
+ * el documento se muere; un lote que falle por red o 5xx se re-encola para el siguiente flush.
516
518
  */
517
519
  flushAnalytics(options?: {
518
520
  final?: boolean;
@@ -523,6 +525,7 @@ declare class DeepdotsPopups {
523
525
  private analyticsIdentity;
524
526
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
525
527
  private setupAnalyticsFlush;
528
+ private startAnalyticsFlushTimer;
526
529
  /** Enable auto-launch functionality with configured triggers */
527
530
  autoLaunch(): void;
528
531
  /** Inicia los triggers configurados */
@@ -602,11 +605,18 @@ declare class DeepdotsPopups {
602
605
  private mapPopupTriggerType;
603
606
  private markSurveyProgress;
604
607
  /**
605
- * Rehidrata el estado de redisplay desde el storage. `answeredSurveys` se reconstruye
606
- * desde las entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
608
+ * Fusiona el estado de redisplay del storage con el de memoria: por cada popup/survey gana
609
+ * la entrada MÁS RECIENTE. Fusionar y no sustituir es lo que hace convivir a varias
610
+ * pestañas sobre el mismo `localStorage`: se llama en el `init()`, antes de cada decisión
611
+ * de mostrar y antes de cada escritura. `answeredSurveys` se reconstruye desde las
612
+ * entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
607
613
  */
608
614
  private loadPopupState;
609
- /** Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria. */
615
+ /**
616
+ * Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria.
617
+ * Fusiona antes de escribir: escribir solo la foto en memoria borraba lo que otra pestaña
618
+ * hubiese guardado desde nuestro `init()`.
619
+ */
610
620
  private persistPopupState;
611
621
  private isPopupDefinition;
612
622
  private isPopupTrigger;
@@ -522,6 +522,29 @@ function ddApplySurveyPrimaryColor(el, style){
522
522
  }
523
523
  });
524
524
 
525
+ // src/ui/busy.ts
526
+ function setSurveyBusy(el, busy) {
527
+ if (!el) return;
528
+ el.style.pointerEvents = busy ? "none" : "";
529
+ el.setAttribute("aria-busy", busy ? "true" : "false");
530
+ if ("inert" in el) {
531
+ el.inert = busy;
532
+ }
533
+ }
534
+ var BUSY_JS;
535
+ var init_busy = __esm({
536
+ "src/ui/busy.ts"() {
537
+ "use strict";
538
+ BUSY_JS = `
539
+ function ddSetSurveyBusy(el, busy){
540
+ if(!el){ return; }
541
+ el.style.pointerEvents = busy ? 'none' : '';
542
+ el.setAttribute('aria-busy', busy ? 'true' : 'false');
543
+ if('inert' in el){ el.inert = busy; }
544
+ }`;
545
+ }
546
+ });
547
+
525
548
  // src/assets/style.css
526
549
  var style_default;
527
550
  var init_style = __esm({
@@ -2964,6 +2987,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
2964
2987
  revealTimer = setTimeout(reveal, REVEAL_TIMEOUT_MS);
2965
2988
  function setLoading(isLoading) {
2966
2989
  spinnerEl.style.display = isLoading ? "flex" : "none";
2990
+ setSurveyBusy(formWrapper, isLoading);
2967
2991
  if (!isLoading) {
2968
2992
  formHost.style.visibility = "visible";
2969
2993
  revealWhenPainted2();
@@ -3119,6 +3143,7 @@ var init_renderPopup = __esm({
3119
3143
  init_font();
3120
3144
  init_logo();
3121
3145
  init_surveyPalette();
3146
+ init_busy();
3122
3147
  init_logger();
3123
3148
  init_labels();
3124
3149
  init_reveal();
@@ -3338,6 +3363,7 @@ init_font();
3338
3363
  init_labels();
3339
3364
  init_reveal();
3340
3365
  init_surveyPalette();
3366
+ init_busy();
3341
3367
  init_style();
3342
3368
  var REVEAL_JS = `
3343
3369
  function ddCreateReveal(popup, onReveal, timeoutMs){
@@ -3557,6 +3583,7 @@ ${customCss}
3557
3583
  var footer=document.getElementById('dd-footer');
3558
3584
  var spinner=document.getElementById('dd-spinner');
3559
3585
  var formHost=document.getElementById('mf');
3586
+ var formWrapper=document.getElementById('dd-form-wrapper');
3560
3587
  var errorHint=document.getElementById('dd-error');
3561
3588
  var backBtn=document.getElementById('dd-back');
3562
3589
  var startBtn=document.getElementById('dd-start');
@@ -3716,6 +3743,7 @@ ${customCss}
3716
3743
  }
3717
3744
 
3718
3745
  ${PRIMARY_COLOR_JS}
3746
+ ${BUSY_JS}
3719
3747
  ${REVEAL_JS}
3720
3748
  // El popup se ense\xF1a cuando el survey est\xE1 pintado; \`ready\` deja al host abrir su propio
3721
3749
  // Modal en ese momento en vez de montar un WebView en blanco.
@@ -3726,6 +3754,11 @@ ${REVEAL_JS}
3726
3754
 
3727
3755
  function setLoading(isLoading){
3728
3756
  spinner.style.display=isLoading?'flex':'none';
3757
+ // El spinner no tapa el survey (28px en posicion absoluta), asi que sin esto el usuario
3758
+ // puede seguir cambiando de opcion mientras se envia la pagina, y ese cambio ya no viaja
3759
+ // con ella. Se bloquea el wrapper, que es el elemento estable: el div de dentro lo
3760
+ // sustituye @magicfeedback/native por su propio contenedor al generar el survey.
3761
+ ddSetSurveyBusy(formWrapper, isLoading);
3729
3762
  if(!isLoading){
3730
3763
  formHost.style.visibility='visible';
3731
3764
  // Fin de la carga, sea por survey cargado o por error. Entre p\xE1ginas es no-op: la
@@ -4331,14 +4364,7 @@ function createFeedbackSink(options) {
4331
4364
  const post = async (body, final, sessionEnd = false) => {
4332
4365
  const json = JSON.stringify(body);
4333
4366
  const small = json.length <= KEEPALIVE_MAX_BYTES;
4334
- if (final && options.sendBeaconImpl && small) {
4335
- const payload = typeof Blob !== "undefined" ? new Blob([json], { type: "application/json" }) : json;
4336
- if (options.sendBeaconImpl(url, payload)) {
4337
- options.log?.("[DeepdotsAnalytics] final flush via sendBeacon");
4338
- return;
4339
- }
4340
- options.log?.("[DeepdotsAnalytics] sendBeacon rejected the batch; falling back to fetch");
4341
- }
4367
+ if (final) options.log?.("[DeepdotsAnalytics] final flush via fetch keepalive");
4342
4368
  const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
4343
4369
  if (!f) {
4344
4370
  options.log?.("[DeepdotsAnalytics] no fetch available; payload not sent", body);
@@ -4348,6 +4374,9 @@ function createFeedbackSink(options) {
4348
4374
  method: "POST",
4349
4375
  headers: { "Content-Type": "application/json" },
4350
4376
  body: json,
4377
+ // La API autentica por `publicKey` en el body y responde `Allow-Origin: *`: una
4378
+ // petición con credenciales fallaría el CORS. Explícito para no depender del default.
4379
+ credentials: "omit",
4351
4380
  // keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.
4352
4381
  ...small ? { keepalive: true } : {}
4353
4382
  });
@@ -4440,6 +4469,15 @@ var NavigationObserver = class {
4440
4469
  this.closeCurrent();
4441
4470
  this.currentScreen = null;
4442
4471
  }
4472
+ /**
4473
+ * Retoma el seguimiento en la pantalla actual tras un `stop()`, cuando se abre una sesión
4474
+ * nueva sin navegar (cambio de usuario, vuelta de la bfcache). No-op si nunca se instaló
4475
+ * (navegación manual por `setScreen`, RN) o si ya hay pantalla en curso.
4476
+ */
4477
+ restart() {
4478
+ if (!this.installed || this.currentScreen != null || typeof window === "undefined") return;
4479
+ this.begin(window.location.href);
4480
+ }
4443
4481
  closeCurrent() {
4444
4482
  if (this.currentScreen == null) return;
4445
4483
  const exit = this.now();
@@ -5083,8 +5121,6 @@ var DeepdotsPopups = class {
5083
5121
  baseUrl: this.baseUrl,
5084
5122
  keys: config.analytics,
5085
5123
  log: (...a) => this.log(...a),
5086
- // sendBeacon: único transporte que sobrevive al cierre de la página.
5087
- sendBeaconImpl: typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" ? (url, body) => navigator.sendBeacon(url, body) : void 0,
5088
5124
  onSessionId: (id) => {
5089
5125
  this.analyticsFeedbackSessionId = id;
5090
5126
  },
@@ -5132,7 +5168,6 @@ var DeepdotsPopups = class {
5132
5168
  this.navObserver.onVisit((v) => this.track("deepdots_page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
5133
5169
  this.navObserver.install();
5134
5170
  this.engagement = new EngagementTracker();
5135
- this.engagement.resume();
5136
5171
  this.setupAnalyticsFlush();
5137
5172
  this.openSession();
5138
5173
  const pendingCrashes = this.crashReporter.drainPendingCrashes();
@@ -5351,6 +5386,7 @@ var DeepdotsPopups = class {
5351
5386
  this.navStarted = false;
5352
5387
  this.analytics?.exitAllMiniServices();
5353
5388
  this.flushEngagement();
5389
+ this.engagement?.pause();
5354
5390
  this.track("deepdots_session_end", { reason });
5355
5391
  this.flushAnalytics({ final: reason === "page_hide", sessionEnd: true });
5356
5392
  this.analyticsFeedbackSessionId = void 0;
@@ -5366,6 +5402,12 @@ var DeepdotsPopups = class {
5366
5402
  if (!this.tracking?.isTrackingEnabled()) return;
5367
5403
  this.sessionOpen = true;
5368
5404
  this.track("deepdots_session_start", {});
5405
+ this.navObserver?.restart();
5406
+ if (this.isDocumentVisible()) this.engagement?.resume();
5407
+ }
5408
+ /** Web: si la pestaña se ve. Sin `document` (RN) manda AppState vía onForeground/onBackground. */
5409
+ isDocumentVisible() {
5410
+ return typeof document === "undefined" || document.visibilityState !== "hidden";
5369
5411
  }
5370
5412
  /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
5371
5413
  previewAnalytics() {
@@ -5378,8 +5420,8 @@ var DeepdotsPopups = class {
5378
5420
  }
5379
5421
  /**
5380
5422
  * Envía el lote acumulado de analytics y vacía el buffer.
5381
- * `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
5382
- * al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
5423
+ * `final: true` (cierre de página/app): el lote no espera al primer POST en vuelo, porque
5424
+ * el documento se muere; un lote que falle por red o 5xx se re-encola para el siguiente flush.
5383
5425
  */
5384
5426
  flushAnalytics(options) {
5385
5427
  if (!this.tracking?.isTrackingEnabled()) return;
@@ -5400,21 +5442,30 @@ var DeepdotsPopups = class {
5400
5442
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
5401
5443
  setupAnalyticsFlush() {
5402
5444
  if (typeof document === "undefined" || typeof window === "undefined") return;
5403
- this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
5445
+ this.startAnalyticsFlushTimer();
5404
5446
  window.addEventListener("pagehide", () => {
5405
5447
  clearInterval(this.analyticsFlushTimer);
5406
5448
  this.closeSession("page_hide");
5407
5449
  });
5450
+ window.addEventListener("pageshow", (event) => {
5451
+ if (!event.persisted) return;
5452
+ this.startAnalyticsFlushTimer();
5453
+ this.openSession();
5454
+ });
5408
5455
  document.addEventListener("visibilitychange", () => {
5409
5456
  if (document.visibilityState === "hidden") {
5410
5457
  this.flushEngagement();
5411
5458
  this.engagement?.pause();
5412
5459
  this.flushAnalytics();
5413
5460
  } else if (document.visibilityState === "visible") {
5414
- this.engagement?.resume();
5461
+ if (this.sessionOpen) this.engagement?.resume();
5415
5462
  }
5416
5463
  });
5417
5464
  }
5465
+ startAnalyticsFlushTimer() {
5466
+ clearInterval(this.analyticsFlushTimer);
5467
+ this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
5468
+ }
5418
5469
  /** Enable auto-launch functionality with configured triggers */
5419
5470
  autoLaunch() {
5420
5471
  if (!this.initialized) {
@@ -5508,6 +5559,7 @@ var DeepdotsPopups = class {
5508
5559
  this.emitEvent("popup_shown", def.surveyId, { popupId: def.id });
5509
5560
  }
5510
5561
  shouldShow(def, pathUrl, skipPathCheck = false) {
5562
+ if (this.storage) this.loadPopupState(this.storage);
5511
5563
  if (this.isPathExcluded(def, pathUrl)) {
5512
5564
  return false;
5513
5565
  }
@@ -6034,22 +6086,32 @@ var DeepdotsPopups = class {
6034
6086
  this.persistPopupState();
6035
6087
  }
6036
6088
  /**
6037
- * Rehidrata el estado de redisplay desde el storage. `answeredSurveys` se reconstruye
6038
- * desde las entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
6089
+ * Fusiona el estado de redisplay del storage con el de memoria: por cada popup/survey gana
6090
+ * la entrada MÁS RECIENTE. Fusionar y no sustituir es lo que hace convivir a varias
6091
+ * pestañas sobre el mismo `localStorage`: se llama en el `init()`, antes de cada decisión
6092
+ * de mostrar y antes de cada escritura. `answeredSurveys` se reconstruye desde las
6093
+ * entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
6039
6094
  */
6040
6095
  loadPopupState(storage) {
6041
6096
  const state = readPopupState(storage);
6042
6097
  Object.entries(state.lastShown).forEach(([popupId, timestamp]) => {
6043
- this.lastShown.set(popupId, timestamp);
6098
+ if (timestamp > (this.lastShown.get(popupId) ?? -Infinity)) this.lastShown.set(popupId, timestamp);
6044
6099
  });
6045
6100
  Object.entries(state.progress).forEach(([surveyId, entry]) => {
6101
+ const current = this.surveyProgress.get(surveyId);
6102
+ if (current && current.timestamp >= entry.timestamp) return;
6046
6103
  this.surveyProgress.set(surveyId, { status: entry.status, timestamp: entry.timestamp });
6047
6104
  if (entry.status === "COMPLETED") this.answeredSurveys.add(surveyId);
6048
6105
  });
6049
6106
  }
6050
- /** Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria. */
6107
+ /**
6108
+ * Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria.
6109
+ * Fusiona antes de escribir: escribir solo la foto en memoria borraba lo que otra pestaña
6110
+ * hubiese guardado desde nuestro `init()`.
6111
+ */
6051
6112
  persistPopupState() {
6052
6113
  if (!this.storage) return;
6114
+ this.loadPopupState(this.storage);
6053
6115
  writePopupState(this.storage, {
6054
6116
  lastShown: Object.fromEntries(this.lastShown),
6055
6117
  progress: Object.fromEntries(this.surveyProgress)