@magicfeedback/popup-sdk 1.9.0 → 1.11.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.
@@ -552,7 +552,7 @@ var init_style = __esm({
552
552
  style_default = `/**
553
553
  * NO EDITAR A MANO \u2014 generado por scripts/vendor-survey-css.mjs
554
554
  *
555
- * Copia de @magicfeedback/native@2.2.22 \u2192 dist/styles/magicfeedback-default.css
555
+ * Copia de @magicfeedback/native@2.2.31 \u2192 dist/styles/magicfeedback-default.css
556
556
  * (la hoja can\xF3nica del paquete, la que apunta su campo "style"), con estos cambios locales:
557
557
  *
558
558
  * 1. Sin el @import de Google Fonts (Nunito): no queremos una petici\xF3n externa desde la
@@ -1536,6 +1536,9 @@ div.magicfeedback-radio {
1536
1536
  width: 100%;
1537
1537
  overflow-x: auto;
1538
1538
  border-radius: var(--mf-radius-md);
1539
+ /* the layout follows the space the survey gets (a pop-in, a sidebar),
1540
+ not the size of the window */
1541
+ container-type: inline-size;
1539
1542
  }
1540
1543
 
1541
1544
  .magicfeedback-multi-question-matrix-table {
@@ -1587,6 +1590,296 @@ div.magicfeedback-radio {
1587
1590
  accent-color: var(--mf-primary);
1588
1591
  }
1589
1592
 
1593
+ .magicfeedback-multi-question-matrix-question {
1594
+ min-width: 200px;
1595
+ padding: 10px;
1596
+ }
1597
+
1598
+ /* the column header names the option in a table; the per-cell label is for
1599
+ screen readers until the rows stack */
1600
+ .magicfeedback-multi-question-matrix-option-label {
1601
+ position: absolute;
1602
+ width: 1px;
1603
+ height: 1px;
1604
+ overflow: hidden;
1605
+ clip: rect(0, 0, 0, 0);
1606
+ white-space: nowrap;
1607
+ }
1608
+
1609
+ /* narrow container: every row becomes its own question with the options
1610
+ stacked underneath, instead of a table cut off at the side */
1611
+ @container (max-width: 599px) {
1612
+ .magicfeedback-multi-question-matrix-table,
1613
+ .magicfeedback-multi-question-matrix-table tbody,
1614
+ .magicfeedback-multi-question-matrix-table tr,
1615
+ .magicfeedback-multi-question-matrix-table td {
1616
+ display: block;
1617
+ width: 100%;
1618
+ }
1619
+
1620
+ .magicfeedback-multi-question-matrix-table thead {
1621
+ display: none;
1622
+ }
1623
+
1624
+ .magicfeedback-multi-question-matrix-row-tr,
1625
+ .magicfeedback-multi-question-matrix-row-tr:nth-child(even) {
1626
+ margin-bottom: var(--mf-space-lg);
1627
+ background: transparent;
1628
+ box-shadow: none;
1629
+ }
1630
+
1631
+ @media (hover: hover) {
1632
+ .magicfeedback-multi-question-matrix-row-tr:hover {
1633
+ margin-bottom: var(--mf-space-lg);
1634
+ background: transparent;
1635
+ box-shadow: none;
1636
+ }
1637
+ }
1638
+
1639
+ .magicfeedback-multi-question-matrix-table td.magicfeedback-multi-question-matrix-question {
1640
+ min-width: 0;
1641
+ padding: 0 0 var(--mf-space-sm);
1642
+ text-align: start;
1643
+ }
1644
+
1645
+ .magicfeedback-multi-question-matrix-question .magicfeedback-multi-question-matrix-label {
1646
+ font-weight: 600;
1647
+ }
1648
+
1649
+ .magicfeedback-multi-question-matrix-table td.magicfeedback-multi-question-matrix-option {
1650
+ display: flex;
1651
+ align-items: center;
1652
+ gap: var(--mf-space-sm);
1653
+ margin-bottom: var(--mf-space-sm);
1654
+ padding: 0 var(--mf-space-md);
1655
+ text-align: start;
1656
+ background: var(--mf-bg-secondary);
1657
+ border: 2px solid var(--mf-border);
1658
+ border-radius: var(--mf-radius-md);
1659
+ cursor: pointer;
1660
+ transition: var(--mf-transition-fast);
1661
+ }
1662
+
1663
+ @media (hover: hover) {
1664
+ .magicfeedback-multi-question-matrix-option:hover {
1665
+ border-color: var(--mf-primary-border);
1666
+ }
1667
+ }
1668
+
1669
+ .magicfeedback-multi-question-matrix-option:has(input:checked) {
1670
+ border-color: var(--mf-primary);
1671
+ background: var(--mf-primary-light);
1672
+ }
1673
+
1674
+ .magicfeedback-multi-question-matrix-table .magicfeedback-multi-question-matrix-option input[type="radio"] {
1675
+ flex-shrink: 0;
1676
+ }
1677
+
1678
+ /* the whole card is the click target */
1679
+ .magicfeedback-multi-question-matrix-option-label {
1680
+ position: static;
1681
+ flex: 1;
1682
+ width: auto;
1683
+ height: auto;
1684
+ padding: var(--mf-space-md) 0;
1685
+ overflow: visible;
1686
+ clip: auto;
1687
+ white-space: normal;
1688
+ font-size: var(--mf-font-size-base);
1689
+ color: var(--mf-text-primary);
1690
+ cursor: pointer;
1691
+ }
1692
+ }
1693
+
1694
+ /* ========================================
1695
+ MAXDIFF (BEST\u2013WORST)
1696
+ ======================================== */
1697
+ .magicfeedback-max-diff {
1698
+ /* "least" gets its own hue, apart from the brand color "most" uses; dark
1699
+ enough to read as header text and to hold the white center dot */
1700
+ --mf-max-diff-worst: #b45309;
1701
+ width: 100%;
1702
+ }
1703
+
1704
+ .magicfeedback-max-diff-container {
1705
+ width: 100%;
1706
+ /* the layout follows the space the survey gets (a pop-in, a sidebar),
1707
+ not the size of the window */
1708
+ container-type: inline-size;
1709
+ }
1710
+
1711
+ .magicfeedback-max-diff-progress {
1712
+ display: flex;
1713
+ align-items: center;
1714
+ gap: var(--mf-space-md);
1715
+ margin-bottom: var(--mf-space-lg);
1716
+ }
1717
+
1718
+ .magicfeedback-max-diff-progress-label {
1719
+ font-size: 0.8125rem;
1720
+ font-weight: var(--mf-font-weight-bold);
1721
+ letter-spacing: 0.04em;
1722
+ text-transform: uppercase;
1723
+ white-space: nowrap;
1724
+ color: var(--mf-text-secondary);
1725
+ }
1726
+
1727
+ .magicfeedback-max-diff-progress-track {
1728
+ flex: 1;
1729
+ height: 6px;
1730
+ overflow: hidden;
1731
+ border-radius: var(--mf-radius-full);
1732
+ background: var(--mf-border);
1733
+ }
1734
+
1735
+ .magicfeedback-max-diff-progress-fill {
1736
+ display: block;
1737
+ height: 100%;
1738
+ border-radius: var(--mf-radius-full);
1739
+ background: var(--mf-primary);
1740
+ }
1741
+
1742
+ .magicfeedback-max-diff-table {
1743
+ width: 100%;
1744
+ border-collapse: collapse;
1745
+ }
1746
+
1747
+ .magicfeedback-max-diff-head,
1748
+ .magicfeedback-max-diff-head-spacer {
1749
+ border-bottom: 1px solid var(--mf-border);
1750
+ }
1751
+
1752
+ .magicfeedback-max-diff-head {
1753
+ width: 11rem;
1754
+ padding: 0 var(--mf-space-sm) var(--mf-space-sm);
1755
+ font-size: 0.8125rem;
1756
+ font-weight: var(--mf-font-weight-extrabold);
1757
+ letter-spacing: 0.04em;
1758
+ text-transform: uppercase;
1759
+ text-align: center;
1760
+ vertical-align: bottom;
1761
+ }
1762
+
1763
+ .magicfeedback-max-diff-head-best {
1764
+ color: var(--mf-primary);
1765
+ }
1766
+
1767
+ .magicfeedback-max-diff-head-worst {
1768
+ color: var(--mf-max-diff-worst);
1769
+ }
1770
+
1771
+ .magicfeedback-max-diff-head-label {
1772
+ display: inline-flex;
1773
+ align-items: center;
1774
+ gap: 6px;
1775
+ }
1776
+
1777
+ .magicfeedback-max-diff-row {
1778
+ transition: background-color 0.15s ease;
1779
+ }
1780
+
1781
+ .magicfeedback-max-diff-row > * {
1782
+ border-bottom: 1px solid var(--mf-border);
1783
+ }
1784
+
1785
+ .magicfeedback-max-diff-row:has(.magicfeedback-max-diff-best:checked) {
1786
+ background-color: color-mix(in srgb, var(--mf-primary) 8%, transparent);
1787
+ }
1788
+
1789
+ .magicfeedback-max-diff-row:has(.magicfeedback-max-diff-worst:checked) {
1790
+ background-color: color-mix(in srgb, var(--mf-max-diff-worst) 8%, transparent);
1791
+ }
1792
+
1793
+ .magicfeedback-max-diff-item {
1794
+ padding: var(--mf-space-md) var(--mf-space-sm);
1795
+ font-size: 1.0625rem;
1796
+ font-weight: var(--mf-font-weight-semibold);
1797
+ line-height: 1.4;
1798
+ text-align: center;
1799
+ color: var(--mf-text-primary);
1800
+ }
1801
+
1802
+ .magicfeedback-max-diff-row:has(input:checked) .magicfeedback-max-diff-item {
1803
+ font-weight: var(--mf-font-weight-extrabold);
1804
+ }
1805
+
1806
+ .magicfeedback-max-diff-cell {
1807
+ padding: 0;
1808
+ }
1809
+
1810
+ /* the whole cell is the click target */
1811
+ .magicfeedback-max-diff-pick {
1812
+ display: flex;
1813
+ align-items: center;
1814
+ justify-content: center;
1815
+ min-height: 64px;
1816
+ cursor: pointer;
1817
+ }
1818
+
1819
+ .magicfeedback-max-diff-input {
1820
+ appearance: none;
1821
+ -webkit-appearance: none;
1822
+ flex-shrink: 0;
1823
+ width: 26px;
1824
+ height: 26px;
1825
+ margin: 0;
1826
+ border: 2px solid color-mix(in srgb, var(--mf-text-secondary) 80%, white);
1827
+ border-radius: var(--mf-radius-full);
1828
+ background: var(--mf-bg-secondary);
1829
+ cursor: pointer;
1830
+ transition: var(--mf-transition-fast);
1831
+ }
1832
+
1833
+ @media (hover: hover) {
1834
+ .magicfeedback-max-diff-pick:hover .magicfeedback-max-diff-input {
1835
+ transform: scale(1.08);
1836
+ }
1837
+ }
1838
+
1839
+ .magicfeedback-max-diff-best:checked {
1840
+ border-color: var(--mf-primary);
1841
+ background: radial-gradient(circle, #FFFFFF 0 4px, var(--mf-primary) 4.5px);
1842
+ }
1843
+
1844
+ .magicfeedback-max-diff-worst:checked {
1845
+ border-color: var(--mf-max-diff-worst);
1846
+ background: radial-gradient(circle, #FFFFFF 0 4px, var(--mf-max-diff-worst) 4.5px);
1847
+ }
1848
+
1849
+ .magicfeedback-max-diff-input:focus-visible {
1850
+ outline: 3px solid var(--mf-primary-border);
1851
+ outline-offset: 3px;
1852
+ }
1853
+
1854
+ /* narrow container: same table, slimmer pick columns and the header icon
1855
+ stacked over its label */
1856
+ @container (max-width: 599px) {
1857
+ .magicfeedback-max-diff-head {
1858
+ width: 4.75rem;
1859
+ padding-inline: 2px;
1860
+ font-size: 0.6875rem;
1861
+ }
1862
+
1863
+ .magicfeedback-max-diff-head-label {
1864
+ flex-direction: column;
1865
+ gap: 2px;
1866
+ }
1867
+
1868
+ .magicfeedback-max-diff-item {
1869
+ padding-inline: 4px;
1870
+ font-size: var(--mf-font-size-base);
1871
+ }
1872
+
1873
+ .magicfeedback-max-diff-pick {
1874
+ min-height: 68px;
1875
+ }
1876
+
1877
+ .magicfeedback-max-diff-input {
1878
+ width: 28px;
1879
+ height: 28px;
1880
+ }
1881
+ }
1882
+
1590
1883
  /* ========================================
1591
1884
  PRIORITY LIST
1592
1885
  ======================================== */
@@ -1777,6 +2070,15 @@ div.magicfeedback-radio {
1777
2070
  }
1778
2071
  }
1779
2072
 
2073
+ /* hidden on the first page; keep the submit button on the right */
2074
+ .magicfeedback-back[hidden] {
2075
+ display: none;
2076
+ }
2077
+
2078
+ .magicfeedback-action-container .magicfeedback-submit {
2079
+ margin-left: auto;
2080
+ }
2081
+
1780
2082
  /* ========================================
1781
2083
  SKIP & COUNTER
1782
2084
  ======================================== */
@@ -2592,6 +2894,9 @@ function ensureFontFace(family, url) {
2592
2894
  }
2593
2895
  if (el.textContent !== css) el.textContent = css;
2594
2896
  }
2897
+ function hasFinePointer() {
2898
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(pointer: fine)").matches;
2899
+ }
2595
2900
  async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId, options) {
2596
2901
  let surveyCompletedEmitted = false;
2597
2902
  let stylesInjected = false;
@@ -3022,6 +3327,13 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
3022
3327
  if (pos.padding) container.style.padding = pos.padding;
3023
3328
  let revealed = false;
3024
3329
  let revealTimer = null;
3330
+ let surveyLoaded = false;
3331
+ let openFocusDone = false;
3332
+ function autofocusOnOpen() {
3333
+ if (openFocusDone || !revealed || !surveyLoaded) return;
3334
+ openFocusDone = true;
3335
+ if (hasFinePointer()) formInstance?.focusFirstQuestion?.();
3336
+ }
3025
3337
  function reveal() {
3026
3338
  if (revealed) return;
3027
3339
  revealed = true;
@@ -3032,6 +3344,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
3032
3344
  if (!container.contains(popup)) return;
3033
3345
  container.style.visibility = "visible";
3034
3346
  container.style.pointerEvents = "";
3347
+ autofocusOnOpen();
3035
3348
  }
3036
3349
  function revealWhenPainted2() {
3037
3350
  if (revealed) return;
@@ -3081,10 +3394,18 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
3081
3394
  // La pantalla final la pinta el popup: `renderSuccess` de @magicfeedback/native usa
3082
3395
  // textContent, así que el mensaje de la plataforma (HTML con imagen) no se vería, y
3083
3396
  // su fallback es un literal genérico que ignora `style.successMessage`.
3084
- addSuccessScreen: false
3397
+ addSuccessScreen: false,
3398
+ // Surveys multi-idioma: native pide a la API este idioma (el del init o el del
3399
+ // dispositivo) y, si el survey no lo tiene, la API sirve el idioma por defecto.
3400
+ lang: options?.language,
3401
+ // Foco en la primera pregunta de escribir tras Start, Siguiente o Atrás. Native lo
3402
+ // aplica después de afterSubmitEvent/onBackEvent, cuando `setLoading(false)` ya ha
3403
+ // quitado el `inert`. La apertura va aparte (ver autofocusOnOpen).
3404
+ autofocus: "navigation"
3085
3405
  };
3086
- generateOptions.onLoadedEvent = ({ formData }) => {
3087
- const surveyLang = Array.isArray(formData?.lang) ? formData.lang.find((l) => typeof l === "string" && l.trim().length > 0) : void 0;
3406
+ generateOptions.onLoadedEvent = ({ formData, lang }) => {
3407
+ const shownLang = typeof lang === "string" && lang.trim().length > 0 ? lang : void 0;
3408
+ const surveyLang = shownLang ?? (Array.isArray(formData?.lang) ? formData.lang.find((l) => typeof l === "string" && l.trim().length > 0) : void 0);
3088
3409
  if (surveyLang) applyLabels(surveyLang);
3089
3410
  try {
3090
3411
  } catch {
@@ -3135,12 +3456,14 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
3135
3456
  updateButtons("in_progress_first");
3136
3457
  }
3137
3458
  updateProgress({ progress: formInstance.progress, total: formInstance.total });
3138
- emit("popup_clicked", surveyId, { action: "loaded" });
3139
3459
  setLoading(false);
3460
+ if (!surveyLoaded) {
3461
+ surveyLoaded = true;
3462
+ autofocusOnOpen();
3463
+ }
3140
3464
  };
3141
3465
  generateOptions.beforeSubmitEvent = () => {
3142
3466
  setLoading(true);
3143
- emit("popup_clicked", surveyId, { action: "before_submit" });
3144
3467
  };
3145
3468
  generateOptions.afterSubmitEvent = ({ error, completed, total, progress, followup }) => {
3146
3469
  const errText = error ? typeof error === "string" ? error : error.message ?? String(error) : "";
@@ -3149,13 +3472,12 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
3149
3472
  if (errText.toLowerCase().includes("no response")) {
3150
3473
  errorHint.textContent = labels.errorRequired;
3151
3474
  errorHint.style.display = "block";
3152
- emit("popup_clicked", surveyId, { action: "validation_error_required" });
3153
3475
  updateNavButtons();
3154
3476
  return;
3155
3477
  }
3156
3478
  errorHint.textContent = labels.errorSubmit;
3157
3479
  errorHint.style.display = "block";
3158
- emit("popup_clicked", surveyId, { action: "submit_error", error: errText });
3480
+ sdkWarn("[Deepdots] survey submit error:", errText);
3159
3481
  return;
3160
3482
  }
3161
3483
  errorHint.style.display = "none";
@@ -3174,7 +3496,6 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
3174
3496
  updateProgress({ progress, total, followup });
3175
3497
  };
3176
3498
  generateOptions.onBackEvent = ({ progress, error, followup }) => {
3177
- emit("popup_clicked", surveyId, { action: "back" });
3178
3499
  if (!error) pageDepth = Math.max(0, pageDepth - 1);
3179
3500
  updateNavButtons();
3180
3501
  updateProgress({ progress, total: formInstance.total, followup });
@@ -3460,7 +3781,7 @@ function trimmedOrNull(value) {
3460
3781
  }
3461
3782
  function buildSurveyHtml(opts) {
3462
3783
  const env = opts.env === "development" ? "dev" : "prod";
3463
- const version = opts.version ?? "2.2.22";
3784
+ const version = opts.version ?? "2.2.31";
3464
3785
  const cdn = `https://cdn.jsdelivr.net/npm/@magicfeedback/native@${version}/dist/magicfeedback-sdk.browser.js`;
3465
3786
  const sid = jsonForScript(opts.surveyId);
3466
3787
  const pid = jsonForScript(opts.productId);
@@ -3798,11 +4119,25 @@ ${customCss}
3798
4119
  ${PRIMARY_COLOR_JS}
3799
4120
  ${BUSY_JS}
3800
4121
  ${REVEAL_JS}
4122
+ // Foco en la primera pregunta si es de escribir. Al navegar lo pone @magicfeedback/native
4123
+ // (autofocus:'navigation'); la apertura la decide el popup, que es quien sabe cu\xE1ndo se revela.
4124
+ // Necesita el popup revelado y el survey cargado, que llegan en cualquier orden, y solo con
4125
+ // rat\xF3n o trackpad: en t\xE1ctil sacar\xEDa el teclado sin que el usuario haya tocado nada.
4126
+ var ddRevealed=false, ddSurveyLoaded=false, ddOpenFocusDone=false;
4127
+ function ddAutofocusOnOpen(){
4128
+ if(ddOpenFocusDone || !ddRevealed || !ddSurveyLoaded){ return; }
4129
+ ddOpenFocusDone=true;
4130
+ var fine=typeof window.matchMedia==='function' && window.matchMedia('(pointer: fine)').matches;
4131
+ var f=window.DeepdotsForm;
4132
+ if(fine && f && typeof f.focusFirstQuestion==='function'){ f.focusFirstQuestion(); }
4133
+ }
3801
4134
  // El popup se ense\xF1a cuando el survey est\xE1 pintado; \`ready\` deja al host abrir su propio
3802
4135
  // Modal en ese momento en vez de montar un WebView en blanco.
3803
4136
  var ddReveal=ddCreateReveal(popup, function(){
3804
4137
  document.body.classList.add('dd-ready');
3805
4138
  emitJSON('ready');
4139
+ ddRevealed=true;
4140
+ ddAutofocusOnOpen();
3806
4141
  }, ${REVEAL_TIMEOUT_MS});
3807
4142
 
3808
4143
  function setLoading(isLoading){
@@ -3840,10 +4175,18 @@ ${REVEAL_JS}
3840
4175
  // textContent, as\xED que el mensaje de la plataforma (HTML con imagen) no se ver\xEDa, y su
3841
4176
  // fallback es un literal gen\xE9rico que ignora style.successMessage.
3842
4177
  addSuccessScreen:false,
4178
+ // Surveys multi-idioma: native pide a la API este idioma (el del init o el del
4179
+ // dispositivo); si el survey no lo tiene, la API sirve el idioma por defecto.
4180
+ lang:${initLangJson}||undefined,
4181
+ // Foco en la primera pregunta de escribir tras Start, Siguiente o Atr\xE1s. Las versiones
4182
+ // de native sin esta opci\xF3n la ignoran.
4183
+ autofocus:'navigation',
3843
4184
  onLoadedEvent:function(args){
3844
- // Idioma del survey: manda sobre el del init para que el chrome no se quede en
3845
- // ingl\xE9s delante de un survey traducido.
3846
- var langs=args && args.formData ? args.formData.lang : null;
4185
+ // Idioma en el que native muestra el survey: manda sobre el del init para que el
4186
+ // chrome no se quede en ingl\xE9s delante de un survey traducido. Native < 2.3 no lo
4187
+ // env\xEDa: entonces el primero de formData.lang (el idioma por defecto).
4188
+ var shown=args && typeof args.lang==='string' && args.lang.trim() ? args.lang : null;
4189
+ var langs=shown ? [shown] : (args && args.formData ? args.formData.lang : null);
3847
4190
  if(langs && langs.length){
3848
4191
  for(var li=0;li<langs.length;li++){
3849
4192
  if(typeof langs[li]==='string' && langs[li].trim()){ ddApplyLabels(langs[li]); break; }
@@ -3897,6 +4240,8 @@ ${REVEAL_JS}
3897
4240
  updateProgress({ progress:form.progress, total:form.total, completed:false, followup:false });
3898
4241
  emitJSON('loaded');
3899
4242
  setLoading(false);
4243
+ // La primera carga es la apertura; las siguientes (tras Start) las enfoca native.
4244
+ if(!ddSurveyLoaded){ ddSurveyLoaded=true; ddAutofocusOnOpen(); }
3900
4245
  },
3901
4246
  beforeSubmitEvent:function(){
3902
4247
  setLoading(true);
@@ -4049,6 +4394,7 @@ var ReactNativePopupRenderer = class {
4049
4394
  this.emitReady(surveyId);
4050
4395
  break;
4051
4396
  case "loaded":
4397
+ break;
4052
4398
  case "before_submit":
4053
4399
  case "after_submit":
4054
4400
  case "back":
@@ -4427,7 +4773,7 @@ async function safeBodyText(res) {
4427
4773
  }
4428
4774
  }
4429
4775
  function createFeedbackSink(options) {
4430
- let feedbackSessionId;
4776
+ let feedbackSessionId = options.initialSessionId || void 0;
4431
4777
  let firstPostInFlight = null;
4432
4778
  let generation = 0;
4433
4779
  const url = `${options.baseUrl}/sdk/feedback`;
@@ -4520,6 +4866,50 @@ function createFeedbackSink(options) {
4520
4866
  };
4521
4867
  }
4522
4868
 
4869
+ // src/analytics/suspended-session.ts
4870
+ var SUSPENDED_SESSION_KEY = "deepdots.analytics.suspended";
4871
+ function getSessionStorage() {
4872
+ try {
4873
+ return typeof window !== "undefined" && window.sessionStorage ? window.sessionStorage : null;
4874
+ } catch {
4875
+ return null;
4876
+ }
4877
+ }
4878
+ function writeSuspendedSession(storage, session) {
4879
+ try {
4880
+ storage.setItem(SUSPENDED_SESSION_KEY, JSON.stringify(session));
4881
+ return true;
4882
+ } catch {
4883
+ return false;
4884
+ }
4885
+ }
4886
+ function clearSuspendedSession(storage) {
4887
+ try {
4888
+ storage.removeItem(SUSPENDED_SESSION_KEY);
4889
+ } catch {
4890
+ }
4891
+ }
4892
+ function takeSuspendedSession(storage, match, now, windowMs) {
4893
+ let raw = null;
4894
+ try {
4895
+ raw = storage.getItem(SUSPENDED_SESSION_KEY);
4896
+ } catch {
4897
+ return null;
4898
+ }
4899
+ if (!raw) return null;
4900
+ clearSuspendedSession(storage);
4901
+ try {
4902
+ const s = JSON.parse(raw);
4903
+ if (typeof s.sessionId !== "string" || !s.sessionId) return null;
4904
+ if (typeof s.at !== "number" || now - s.at < 0 || now - s.at > windowMs) return null;
4905
+ if (s.integration !== match.integration) return null;
4906
+ if ((s.userId ?? null) !== match.userId) return null;
4907
+ return { sessionId: s.sessionId, integration: s.integration, userId: s.userId ?? null, at: s.at };
4908
+ } catch {
4909
+ return null;
4910
+ }
4911
+ }
4912
+
4523
4913
  // src/tracking/navigation-observer.ts
4524
4914
  var UUID_PREFIX = /^[0-9a-f]{8}-[0-9a-f]{4}-/i;
4525
4915
  function screenPath(href) {
@@ -5118,8 +5508,10 @@ var ANALYTICS_HEARTBEAT_MS = 5 * 6e4;
5118
5508
  var ANALYTICS_SESSION_TIMEOUT_MS = 30 * 6e4;
5119
5509
  var ANALYTICS_SERVER_IDLE_MS = 60 * 6e4;
5120
5510
  var ANALYTICS_SESSION_DISCARD_MS = ANALYTICS_SERVER_IDLE_MS - 10 * 6e4;
5511
+ var ANALYTICS_RESUME_WINDOW_MS = 6e4;
5121
5512
  var RESERVED_EVENT_PREFIX = "deepdots_";
5122
5513
  var CUSTOM_EVENT_PREFIX = "deepdots_event_";
5514
+ var MAX_PENDING_TRIGGERS = 20;
5123
5515
  var DeepdotsPopups = class {
5124
5516
  constructor() {
5125
5517
  this.config = null;
@@ -5133,6 +5525,14 @@ var DeepdotsPopups = class {
5133
5525
  // deprecated: mantenido para compatibilidad interna
5134
5526
  this.popupDefinitions = [];
5135
5527
  this.popupsLoaded = false;
5528
+ /**
5529
+ * `true` mientras el `GET /sdk/{apiKey}/popups` de `init()` está en vuelo. En esa ventana los
5530
+ * disparos del host (`triggerEvent`, `triggerSurvey`) no tienen contra qué evaluarse, así que
5531
+ * se guardan en `pendingTriggers` y se reproducen al cargar. Click, scroll, time y exit no
5532
+ * hacen falta: son listeners que `autoLaunch` instala ya con las definiciones cargadas.
5533
+ */
5534
+ this.awaitingPopups = false;
5535
+ this.pendingTriggers = [];
5136
5536
  this.pendingAutoLaunch = false;
5137
5537
  // Estado que alimenta las reglas de redisplay. Copia de trabajo en memoria; la fuente
5138
5538
  // duradera es el storage (`popup-state-store`), rehidratada en `init()` — sin eso, una
@@ -5187,6 +5587,13 @@ var DeepdotsPopups = class {
5187
5587
  this.lastEngagementAt = 0;
5188
5588
  /** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
5189
5589
  this.hiddenAt = null;
5590
+ /**
5591
+ * Sesión suspendida por un `pagehide` (ms epoch y `deepdots_user_id`); null si no hay.
5592
+ * `openSession` la reanuda si sigue a tiempo (ver ANALYTICS_RESUME_WINDOW_MS).
5593
+ */
5594
+ this.suspended = null;
5595
+ /** Integración de analytics del init: la sesión suspendida solo la hereda la misma. */
5596
+ this.analyticsIntegration = void 0;
5190
5597
  /**
5191
5598
  * Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
5192
5599
  * verdad para el context de analytics Y para la segmentación por idioma de los popups.
@@ -5246,10 +5653,17 @@ var DeepdotsPopups = class {
5246
5653
  }
5247
5654
  }
5248
5655
  this.log("tracking \xB7 user_id:", this.tracking.getUserId(), "\xB7 new_user:", this.tracking.isNewUser(), "\xB7 enabled:", this.tracking.isTrackingEnabled());
5656
+ this.analyticsIntegration = config.analytics?.integration;
5657
+ const resumable = this.readResumableSession();
5658
+ if (resumable) {
5659
+ this.analyticsFeedbackSessionId = resumable.sessionId;
5660
+ this.suspended = { at: resumable.at, userId: resumable.userId };
5661
+ }
5249
5662
  const analyticsSink = config.analytics ? createFeedbackSink({
5250
5663
  baseUrl: this.baseUrl,
5251
5664
  keys: config.analytics,
5252
5665
  log: (...a) => this.log(...a),
5666
+ initialSessionId: resumable?.sessionId,
5253
5667
  onSessionId: (id) => {
5254
5668
  this.analyticsFeedbackSessionId = id;
5255
5669
  this.notifyFeedbackSession(id, "open");
@@ -5299,6 +5713,9 @@ var DeepdotsPopups = class {
5299
5713
  this.engagement = new EngagementTracker();
5300
5714
  this.setupAnalyticsFlush();
5301
5715
  this.openSession();
5716
+ if (resumable && this.analyticsFeedbackSessionId === resumable.sessionId) {
5717
+ this.notifyFeedbackSession(resumable.sessionId, "open");
5718
+ }
5302
5719
  const pendingCrashes = this.crashReporter.drainPendingCrashes();
5303
5720
  if (this.tracking?.isTrackingEnabled()) {
5304
5721
  for (const rec of pendingCrashes) {
@@ -5309,8 +5726,10 @@ var DeepdotsPopups = class {
5309
5726
  this.log("SDK initialized", this.config);
5310
5727
  if (this.renderer.init) this.renderer.init();
5311
5728
  this.setupPopupContainer();
5729
+ this.awaitingPopups = Boolean(this.config.apiKey && this.baseUrl);
5312
5730
  this.fetchPopupsFromServer().then((defs) => {
5313
- this.popupDefinitions = this.validatePopupDefinitions(defs);
5731
+ if (!defs) this.dropPendingTriggers();
5732
+ this.popupDefinitions = this.validatePopupDefinitions(defs ?? []);
5314
5733
  this.popupsLoaded = true;
5315
5734
  this.log("Popups loaded from API", this.popupDefinitions);
5316
5735
  if (this.popupDefinitions.length) this.renderer.preload?.();
@@ -5319,6 +5738,7 @@ var DeepdotsPopups = class {
5319
5738
  if (this.pendingAutoLaunch) {
5320
5739
  this.startTriggers();
5321
5740
  }
5741
+ this.replayPendingTriggers();
5322
5742
  });
5323
5743
  }
5324
5744
  /** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
@@ -5547,9 +5967,15 @@ var DeepdotsPopups = class {
5547
5967
  openSession() {
5548
5968
  if (this.sessionOpen) return;
5549
5969
  if (!this.tracking?.isTrackingEnabled()) return;
5970
+ const suspendedMs = this.takeSuspendedSession();
5550
5971
  this.sessionOpen = true;
5551
5972
  this.lastEngagementAt = Date.now();
5552
- this.track("deepdots_session_start", {});
5973
+ if (suspendedMs === null) {
5974
+ this.track("deepdots_session_start", {});
5975
+ } else {
5976
+ this.track("deepdots_session_resume", { suspended_ms: suspendedMs });
5977
+ this.log("tracking \xB7 session resumed after", suspendedMs, "ms:", this.analyticsFeedbackSessionId);
5978
+ }
5553
5979
  this.navObserver?.restart();
5554
5980
  if (this.isDocumentVisible()) this.engagement?.resume();
5555
5981
  }
@@ -5614,6 +6040,75 @@ var DeepdotsPopups = class {
5614
6040
  }
5615
6041
  this.openSession();
5616
6042
  }
6043
+ /**
6044
+ * `pagehide` en web: SUSPENDE la sesión en vez de cerrarla, porque el navegador lo lanza
6045
+ * igual al recargar que al cerrar la pestaña. Vuelca lo pendiente (page_view de la
6046
+ * pantalla, mini-services, engagement) con un `deepdots_session_suspend` y SIN
6047
+ * `completed`, y deja el registro en `sessionStorage` (ver `suspended-session.ts`):
6048
+ * - si la página vuelve a cargar en esta pestaña, `init()` sigue en el mismo registro;
6049
+ * - si la pestaña se cerró, el backend lo cierra a los pocos minutos sin otro lote.
6050
+ *
6051
+ * Devuelve false cuando no se puede suspender — aún no se conoce el registro (el primer
6052
+ * POST sigue en vuelo) o no hay `sessionStorage` — y entonces el caller cierra como antes.
6053
+ */
6054
+ suspendSession() {
6055
+ if (!this.sessionOpen || !this.tracking?.isTrackingEnabled()) return false;
6056
+ const sessionId = this.analyticsFeedbackSessionId;
6057
+ const integration = this.analyticsIntegration;
6058
+ const storage = getSessionStorage();
6059
+ if (!sessionId || !integration || !storage) return false;
6060
+ const at = Date.now();
6061
+ const userId = this.tracking.getUserId();
6062
+ if (!writeSuspendedSession(storage, { sessionId, integration, userId, at })) return false;
6063
+ this.sessionOpen = false;
6064
+ this.suspended = { at, userId };
6065
+ this.navObserver?.stop();
6066
+ this.navStarted = false;
6067
+ this.analytics?.exitAllMiniServices();
6068
+ this.flushEngagement();
6069
+ this.engagement?.pause();
6070
+ this.track("deepdots_session_suspend", { reason: "page_hide" });
6071
+ this.flushAnalytics({ final: true });
6072
+ this.log("tracking \xB7 session suspended:", sessionId);
6073
+ return true;
6074
+ }
6075
+ /**
6076
+ * Recoge la sesión que dejó suspendida la carga anterior de esta pestaña (ver
6077
+ * `suspendSession`). Solo con analytics configurado y el tracking activo: sin ellos no hay
6078
+ * registro en el que seguir.
6079
+ */
6080
+ readResumableSession() {
6081
+ const integration = this.analyticsIntegration;
6082
+ const storage = getSessionStorage();
6083
+ if (!integration || !storage || !this.tracking?.isTrackingEnabled()) return null;
6084
+ return takeSuspendedSession(
6085
+ storage,
6086
+ { integration, userId: this.tracking.getUserId() },
6087
+ Date.now(),
6088
+ ANALYTICS_RESUME_WINDOW_MS
6089
+ );
6090
+ }
6091
+ /**
6092
+ * Consume la sesión suspendida al abrir: los ms que estuvo suspendida si se puede seguir en
6093
+ * ella (mismo usuario, dentro de ANALYTICS_RESUME_WINDOW_MS); null si no había o ya no toca.
6094
+ * En ese caso se olvida su registro sin enviar nada: lo cierra el backend, y la sesión que
6095
+ * se abre ahora va a uno nuevo.
6096
+ */
6097
+ takeSuspendedSession() {
6098
+ const suspended = this.suspended;
6099
+ if (!suspended) return null;
6100
+ this.suspended = null;
6101
+ const storage = getSessionStorage();
6102
+ if (storage) clearSuspendedSession(storage);
6103
+ const suspendedMs = Math.max(0, Date.now() - suspended.at);
6104
+ if (this.analyticsFeedbackSessionId && suspendedMs <= ANALYTICS_RESUME_WINDOW_MS && suspended.userId === (this.tracking?.getUserId() ?? null)) {
6105
+ return suspendedMs;
6106
+ }
6107
+ this.analytics?.discardSession(this.analyticsIdentity());
6108
+ this.analyticsFeedbackSessionId = void 0;
6109
+ this.tracking?.setSessionId(null);
6110
+ return null;
6111
+ }
5617
6112
  /** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
5618
6113
  discardSession() {
5619
6114
  this.sessionOpen = false;
@@ -5662,7 +6157,7 @@ var DeepdotsPopups = class {
5662
6157
  if (typeof document === "undefined" || typeof window === "undefined") return;
5663
6158
  window.addEventListener("pagehide", () => {
5664
6159
  clearInterval(this.analyticsFlushTimer);
5665
- this.closeSession("page_hide");
6160
+ if (!this.suspendSession()) this.closeSession("page_hide");
5666
6161
  });
5667
6162
  window.addEventListener("pageshow", (event) => {
5668
6163
  if (!event.persisted) return;
@@ -5730,6 +6225,7 @@ var DeepdotsPopups = class {
5730
6225
  }
5731
6226
  /** Lógica para evaluar condiciones antes de mostrar una encuesta */
5732
6227
  triggerSurvey(surveyId, popupId) {
6228
+ if (this.bufferIfAwaitingPopups({ kind: "survey", surveyId, popupId })) return;
5733
6229
  const def = this.findPopupDefinition(surveyId, popupId);
5734
6230
  if (!def) {
5735
6231
  this.debug("No popup definition for trigger", { surveyId, popupId });
@@ -5751,6 +6247,10 @@ var DeepdotsPopups = class {
5751
6247
  this.debug("Ignoring empty event trigger name");
5752
6248
  return;
5753
6249
  }
6250
+ if (this.bufferIfAwaitingPopups({ kind: "event", name: normalized })) return;
6251
+ this.evaluateEvent(normalized);
6252
+ }
6253
+ evaluateEvent(normalized) {
5754
6254
  const candidates = this.popupDefinitions.filter((def) => {
5755
6255
  return def.triggers.some((trigger) => {
5756
6256
  const triggerValue = String(trigger.value ?? "").trim();
@@ -5768,6 +6268,40 @@ var DeepdotsPopups = class {
5768
6268
  }
5769
6269
  this.showDefinition(matched);
5770
6270
  }
6271
+ /**
6272
+ * Guarda `trigger` si las definiciones aún no han llegado. Devuelve `true` si lo guardó (el
6273
+ * llamador no debe evaluarlo ahora) y `false` si no hay carga en vuelo (se evalúa ya, como
6274
+ * siempre: tras cargar, tras un fallo o sin apiKey).
6275
+ */
6276
+ bufferIfAwaitingPopups(trigger) {
6277
+ if (!this.awaitingPopups) return false;
6278
+ if (this.pendingTriggers.length >= MAX_PENDING_TRIGGERS) {
6279
+ const dropped = this.pendingTriggers.shift();
6280
+ this.debug("Pending trigger buffer full; dropped oldest", dropped);
6281
+ }
6282
+ this.pendingTriggers.push(trigger);
6283
+ this.debug("Popups not loaded yet; trigger buffered", trigger);
6284
+ return true;
6285
+ }
6286
+ /** La carga falló: sin definiciones no hay nada que reproducir, se descarta todo. */
6287
+ dropPendingTriggers() {
6288
+ const dropped = this.pendingTriggers.length;
6289
+ this.awaitingPopups = false;
6290
+ this.pendingTriggers = [];
6291
+ if (dropped > 0) this.debug("Popups failed to load; dropped pending triggers", dropped);
6292
+ }
6293
+ /** Reproduce en orden por la evaluación normal (segmentos, cooldowns y condiciones de AHORA). */
6294
+ replayPendingTriggers() {
6295
+ this.awaitingPopups = false;
6296
+ const replay = this.pendingTriggers;
6297
+ this.pendingTriggers = [];
6298
+ if (!replay.length) return;
6299
+ this.debug("Replaying triggers received before popups loaded", replay.length);
6300
+ replay.forEach((trigger) => {
6301
+ if (trigger.kind === "event") this.evaluateEvent(trigger.name);
6302
+ else this.triggerSurvey(trigger.surveyId, trigger.popupId);
6303
+ });
6304
+ }
5771
6305
  showDefinition(def) {
5772
6306
  this.log("Showing popup (definition)", def);
5773
6307
  this.surveyToPopupId.set(def.surveyId, def.id);
@@ -6038,7 +6572,7 @@ var DeepdotsPopups = class {
6038
6572
  } catch {
6039
6573
  }
6040
6574
  }
6041
- /** Fetch al servidor para obtener popups */
6575
+ /** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */
6042
6576
  async fetchPopupsFromServer() {
6043
6577
  const apiKey = this.config?.apiKey;
6044
6578
  const baseUrl = this.baseUrl;
@@ -6054,19 +6588,19 @@ var DeepdotsPopups = class {
6054
6588
  const response = await fetch(endpoint);
6055
6589
  if (!response.ok) {
6056
6590
  this.log("Failed to fetch popups", response.status, response.statusText);
6057
- return [];
6591
+ return null;
6058
6592
  }
6059
6593
  const raw = await response.text();
6060
6594
  const parsed = JSON.parse(raw);
6061
6595
  if (!Array.isArray(parsed)) {
6062
6596
  this.log("Unexpected popups payload", parsed);
6063
- return [];
6597
+ return null;
6064
6598
  }
6065
6599
  this.debug("Fetched popups payload", parsed);
6066
6600
  return parsed;
6067
6601
  } catch (error) {
6068
6602
  this.log("Error fetching popups", error);
6069
- return [];
6603
+ return null;
6070
6604
  }
6071
6605
  }
6072
6606
  /** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
@@ -6490,6 +7024,7 @@ function DeepdotsProvider({ config, children }) {
6490
7024
  style: { flex: 1 },
6491
7025
  originWhitelist: ["*"],
6492
7026
  javaScriptEnabled: true,
7027
+ keyboardDisplayRequiresUserAction: false,
6493
7028
  source: { html: survey.html, baseUrl: "https://sdk.deepdots.com/" },
6494
7029
  onMessage: (e) => rendererRef.current?.handleMessage(e.nativeEvent.data)
6495
7030
  }