@active-reach/web-sdk 1.25.0 → 1.26.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/dist/index.js CHANGED
@@ -265,6 +265,76 @@ function lucideSvg(name, opts = {}) {
265
265
  }).join("");
266
266
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${escapeAttr(color)}" stroke-width="${strokeWidth}" stroke-linecap="round" stroke-linejoin="round"${cls} ${a11y}>${children}</svg>`;
267
267
  }
268
+ function npsBandFromScore(score) {
269
+ if (score <= 6) return "detractor";
270
+ if (score <= 8) return "passive";
271
+ return "promoter";
272
+ }
273
+ function computeProgress(input) {
274
+ const threshold = input.threshold > 0 ? input.threshold : 0;
275
+ const current = Number.isFinite(input.current) && input.current > 0 ? input.current : 0;
276
+ const pct = threshold > 0 ? Math.max(0, Math.min(100, current / threshold * 100)) : 0;
277
+ const complete = threshold > 0 && pct >= 100;
278
+ const remaining = threshold > 0 ? Math.max(0, threshold - current) : 0;
279
+ const milestonesReached = [];
280
+ if (threshold > 0) {
281
+ if (pct >= 25) milestonesReached.push("q25");
282
+ if (pct >= 50) milestonesReached.push("q50");
283
+ if (pct >= 75) milestonesReached.push("q75");
284
+ if (pct >= 100) milestonesReached.push("unlocked");
285
+ }
286
+ return { pct, complete, remaining, milestonesReached };
287
+ }
288
+ function cardProgressSubLabel(i) {
289
+ const { complete, remaining, pct } = computeProgress(i);
290
+ const threshold = i.threshold > 0 ? i.threshold : 0;
291
+ if (complete) return { text: i.rewardText || "Unlocked!", complete: true };
292
+ if (threshold <= 0) return { text: i.rewardText || "", complete: false };
293
+ const sym = i.goalType === "cart_total" ? i.currencySymbol || "" : "";
294
+ const pctStr = i.showPct ? ` · ${Math.round(pct)}%` : "";
295
+ return {
296
+ text: `Shop for ${sym}${remaining.toFixed(0)} more${pctStr}`,
297
+ complete: false
298
+ };
299
+ }
300
+ function readGoalFromCart(goalType, cart) {
301
+ if (!cart) return void 0;
302
+ if (goalType === "items_in_cart") return Number(cart.itemCount) || 0;
303
+ if (goalType === "cart_total") return Number(cart.total) || 0;
304
+ return void 0;
305
+ }
306
+ function computeCountdown(targetMs, nowMs) {
307
+ const totalMs = Number.isFinite(targetMs) ? Math.max(0, targetMs - nowMs) : 0;
308
+ const expired = !(targetMs > nowMs);
309
+ const totalSec = Math.floor(totalMs / 1e3);
310
+ const days = Math.floor(totalSec / 86400);
311
+ const hours = Math.floor(totalSec % 86400 / 3600);
312
+ const minutes = Math.floor(totalSec % 3600 / 60);
313
+ const seconds = totalSec % 60;
314
+ const pad = (n) => n < 10 ? "0" + n : "" + n;
315
+ const display = days > 0 ? days + "d " + pad(hours) + ":" + pad(minutes) + ":" + pad(seconds) : pad(hours) + ":" + pad(minutes) + ":" + pad(seconds);
316
+ return { totalMs, expired, days, hours, minutes, seconds, display };
317
+ }
318
+ function createFlowAccumulator() {
319
+ const buffer = [];
320
+ let completed = false;
321
+ return {
322
+ recordStep(r) {
323
+ const idx = buffer.findIndex((b) => b.step_id === r.step_id);
324
+ if (idx >= 0) buffer[idx] = r;
325
+ else buffer.push(r);
326
+ },
327
+ steps() {
328
+ return buffer.slice();
329
+ },
330
+ consolidated() {
331
+ return { steps: buffer.slice(), step_count: buffer.length, completed };
332
+ },
333
+ markCompleted() {
334
+ completed = true;
335
+ }
336
+ };
337
+ }
268
338
  const WEIGHT = {
269
339
  regular: "400",
270
340
  medium: "500",
@@ -436,7 +506,6 @@ function buildCountdownElement(el, fg, muted) {
436
506
  time.style.cssText = `font-size:15px;font-weight:700;color:${fg};letter-spacing:0.2px;`;
437
507
  wrap.appendChild(label);
438
508
  wrap.appendChild(time);
439
- const pad = (n) => String(n).padStart(2, "0");
440
509
  let started = false;
441
510
  let timer = 0;
442
511
  const tick = () => {
@@ -444,25 +513,71 @@ function buildCountdownElement(el, fg, muted) {
444
513
  if (timer) clearInterval(timer);
445
514
  return;
446
515
  }
447
- const ms = target - Date.now();
448
- if (ms <= 0) {
449
- time.textContent = "00:00:00";
450
- if (timer) clearInterval(timer);
451
- return;
452
- }
453
- const s = Math.floor(ms / 1e3);
454
- const d = Math.floor(s / 86400);
455
- const h = Math.floor(s % 86400 / 3600);
456
- const m2 = Math.floor(s % 3600 / 60);
457
- const ss = s % 60;
458
- time.textContent = d > 0 ? `${d}d ${pad(h)}:${pad(m2)}:${pad(ss)}` : `${pad(h)}:${pad(m2)}:${pad(ss)}`;
516
+ const cd = computeCountdown(target, Date.now());
517
+ time.textContent = cd.display;
518
+ if (cd.expired && timer) clearInterval(timer);
459
519
  started = true;
460
520
  };
461
521
  tick();
462
522
  timer = setInterval(tick, 1e3);
463
523
  return wrap;
464
524
  }
525
+ function appendRatingThanks(wrap, el, ctx, fg, accent, isHigh) {
526
+ appendThanks(wrap, el, fg, "Thanks for rating us!");
527
+ const url = isHigh && typeof el.rating_high_url === "string" ? ctx.sanitizeUrl(el.rating_high_url) : "";
528
+ if (url) {
529
+ const cta = document.createElement("button");
530
+ cta.type = "button";
531
+ cta.textContent = el.rating_high_cta || "Leave a review";
532
+ cta.style.cssText = `margin:4px auto 0;display:block;padding:9px 18px;border:none;border-radius:999px;background:${accent};color:#fff;font-size:13px;font-weight:800;cursor:pointer;`;
533
+ cta.addEventListener("click", () => {
534
+ ctx.trackEvent(ctx.campaign.id, "clicked", { meta: { element: "rating_input", cta: "high_review" } });
535
+ if (ctx.navigate) ctx.navigate(url, "cta");
536
+ else window.location.href = url;
537
+ });
538
+ wrap.appendChild(cta);
539
+ }
540
+ }
541
+ function renderRatingFollowupStep(wrap, el, ctx, stars, fg, accent) {
542
+ while (wrap.firstChild) wrap.removeChild(wrap.firstChild);
543
+ const heading = document.createElement("div");
544
+ heading.textContent = el.rating_followup_prompt || "Sorry to hear that — what went wrong?";
545
+ heading.style.cssText = `font-size:13px;font-weight:700;color:${fg};text-align:center;margin-bottom:8px;`;
546
+ wrap.appendChild(heading);
547
+ const ta = document.createElement("textarea");
548
+ ta.rows = 3;
549
+ ta.placeholder = "Tell us more…";
550
+ ta.style.cssText = `width:100%;box-sizing:border-box;padding:10px;border-radius:10px;border:1px solid color-mix(in srgb, ${accent} 30%, transparent);background:transparent;color:${fg};font-size:13px;resize:vertical;margin-bottom:10px;font-family:inherit;`;
551
+ wrap.appendChild(ta);
552
+ let done = false;
553
+ const finish = (withComment) => {
554
+ var _a, _b;
555
+ if (done) return;
556
+ done = true;
557
+ const comment = withComment ? ta.value.trim() : "";
558
+ void ((_a = ctx.submitResponse) == null ? void 0 : _a.call(ctx, "rating", comment ? { stars, comment } : { stars }));
559
+ (_b = ctx.playReaction) == null ? void 0 : _b.call(ctx, wrap, "acknowledge");
560
+ appendThanks(wrap, el, fg, "Thanks — we’ll do better.");
561
+ };
562
+ const btnRow = document.createElement("div");
563
+ btnRow.style.cssText = "display:flex;gap:8px;justify-content:center;";
564
+ const send = document.createElement("button");
565
+ send.type = "button";
566
+ send.textContent = "Send";
567
+ send.style.cssText = `padding:9px 20px;border-radius:999px;border:none;background:${accent};color:#fff;font-weight:800;font-size:13px;cursor:pointer;`;
568
+ send.addEventListener("click", () => finish(true));
569
+ const skip = document.createElement("button");
570
+ skip.type = "button";
571
+ skip.textContent = "Skip";
572
+ skip.style.cssText = `padding:9px 16px;border-radius:999px;border:1px solid color-mix(in srgb, ${accent} 30%, transparent);background:transparent;color:${fg};font-weight:600;font-size:13px;cursor:pointer;`;
573
+ skip.addEventListener("click", () => finish(false));
574
+ btnRow.appendChild(send);
575
+ btnRow.appendChild(skip);
576
+ wrap.appendChild(btnRow);
577
+ ta.focus();
578
+ }
465
579
  function buildRatingInputElement(el, ctx, fg) {
580
+ const accent = ctx.sanitizeColor(ctx.campaign.background_color || "#4169e1");
466
581
  const wrap = document.createElement("div");
467
582
  wrap.style.cssText = "display:flex;flex-direction:column;gap:6px;margin:2px 0;";
468
583
  if (el.content) {
@@ -505,9 +620,14 @@ function buildRatingInputElement(el, ctx, fg) {
505
620
  submitted = true;
506
621
  paint(i);
507
622
  ctx.trackEvent(ctx.campaign.id, "clicked", { meta: { element: "rating_input", stars: i } });
508
- void ((_a = ctx.submitResponse) == null ? void 0 : _a.call(ctx, "rating", { stars: i }));
509
- (_b = ctx.playReaction) == null ? void 0 : _b.call(ctx, wrap, i >= Math.ceil(scale * 0.6) ? "affirm" : "acknowledge");
510
- appendThanks(wrap, el, fg, "Thanks for rating us!");
623
+ const lowThreshold = typeof el.rating_followup_threshold === "number" ? el.rating_followup_threshold : Math.ceil(scale * 0.8);
624
+ if (el.rating_followup_enabled === true && i <= lowThreshold) {
625
+ renderRatingFollowupStep(wrap, el, ctx, i, fg, accent);
626
+ } else {
627
+ void ((_a = ctx.submitResponse) == null ? void 0 : _a.call(ctx, "rating", { stars: i }));
628
+ (_b = ctx.playReaction) == null ? void 0 : _b.call(ctx, wrap, i >= Math.ceil(scale * 0.6) ? "affirm" : "acknowledge");
629
+ appendRatingThanks(wrap, el, ctx, fg, accent, i >= Math.ceil(scale * 0.6));
630
+ }
511
631
  });
512
632
  row.appendChild(btn);
513
633
  }
@@ -566,6 +686,85 @@ function buildPollElement(el, ctx, fg, accent) {
566
686
  });
567
687
  return wrap;
568
688
  }
689
+ function appendNpsThanks(wrap, el, ctx, fg, accent, isPromoter) {
690
+ while (wrap.firstChild) wrap.removeChild(wrap.firstChild);
691
+ const note = document.createElement("div");
692
+ note.setAttribute("role", "status");
693
+ note.textContent = (typeof el.thanks_text === "string" && el.thanks_text.trim() ? el.thanks_text : "") || (isPromoter ? "Thank you! We’d love a public review." : "Thanks — your feedback helps us improve.");
694
+ note.style.cssText = `font-size:13px;font-weight:700;color:${fg};text-align:center;margin:6px 0;`;
695
+ wrap.appendChild(note);
696
+ const url = isPromoter && typeof el.nps_promoter_url === "string" ? ctx.sanitizeUrl(el.nps_promoter_url) : "";
697
+ if (url) {
698
+ const cta = document.createElement("button");
699
+ cta.type = "button";
700
+ cta.textContent = el.nps_promoter_cta || "Leave a review";
701
+ cta.style.cssText = `margin:4px auto 0;display:block;padding:9px 18px;border:none;border-radius:999px;background:${accent};color:#fff;font-size:13px;font-weight:800;cursor:pointer;`;
702
+ cta.addEventListener("click", () => {
703
+ ctx.trackEvent(ctx.campaign.id, "clicked", { meta: { element: "nps", cta: "promoter_review" } });
704
+ if (ctx.navigate) ctx.navigate(url, "cta");
705
+ else window.location.href = url;
706
+ });
707
+ wrap.appendChild(cta);
708
+ }
709
+ }
710
+ function renderNpsReasonStep(wrap, el, ctx, score, fg, accent) {
711
+ const promoterThreshold = typeof el.nps_promoter_threshold === "number" ? el.nps_promoter_threshold : 9;
712
+ const band = npsBandFromScore(score);
713
+ const prompt = score >= promoterThreshold ? el.nps_follow_up_promoter || "What do you love most about us?" : score <= 6 ? el.nps_follow_up_detractor || "How can we improve?" : el.nps_follow_up_passive || "What would make us a 10?";
714
+ while (wrap.firstChild) wrap.removeChild(wrap.firstChild);
715
+ const heading = document.createElement("div");
716
+ heading.textContent = prompt;
717
+ heading.style.cssText = `font-size:13px;font-weight:700;color:${fg};text-align:center;margin-bottom:8px;`;
718
+ wrap.appendChild(heading);
719
+ const ta = document.createElement("textarea");
720
+ const chips = Array.isArray(el.nps_reason_chips) ? el.nps_reason_chips : [];
721
+ if (chips.length) {
722
+ const chipRow = document.createElement("div");
723
+ chipRow.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin-bottom:8px;";
724
+ chips.slice(0, 8).forEach((c) => {
725
+ const chip = document.createElement("button");
726
+ chip.type = "button";
727
+ chip.textContent = c;
728
+ chip.style.cssText = `padding:5px 11px;border-radius:999px;border:1px solid color-mix(in srgb, ${accent} 35%, transparent);background:transparent;color:${fg};font-size:12px;cursor:pointer;`;
729
+ chip.addEventListener("click", () => {
730
+ ta.value = ta.value ? `${ta.value}, ${c}` : c;
731
+ ta.focus();
732
+ });
733
+ chipRow.appendChild(chip);
734
+ });
735
+ wrap.appendChild(chipRow);
736
+ }
737
+ ta.rows = 3;
738
+ ta.placeholder = "Share a little more…";
739
+ ta.style.cssText = `width:100%;box-sizing:border-box;padding:10px;border-radius:10px;border:1px solid color-mix(in srgb, ${accent} 30%, transparent);background:transparent;color:${fg};font-size:13px;resize:vertical;margin-bottom:10px;font-family:inherit;`;
740
+ wrap.appendChild(ta);
741
+ let done = false;
742
+ const finish = (withComment) => {
743
+ var _a, _b;
744
+ if (done) return;
745
+ done = true;
746
+ const comment = withComment ? ta.value.trim() : "";
747
+ void ((_a = ctx.submitResponse) == null ? void 0 : _a.call(ctx, "nps", comment ? { score, band, comment } : { score, band }));
748
+ (_b = ctx.playReaction) == null ? void 0 : _b.call(ctx, wrap, score >= promoterThreshold ? "affirm" : score <= 6 ? "empathize" : "acknowledge");
749
+ appendNpsThanks(wrap, el, ctx, fg, accent, score >= promoterThreshold);
750
+ };
751
+ const btnRow = document.createElement("div");
752
+ btnRow.style.cssText = "display:flex;gap:8px;justify-content:center;";
753
+ const send = document.createElement("button");
754
+ send.type = "button";
755
+ send.textContent = "Send";
756
+ send.style.cssText = `padding:9px 20px;border-radius:999px;border:none;background:${accent};color:#fff;font-weight:800;font-size:13px;cursor:pointer;`;
757
+ send.addEventListener("click", () => finish(true));
758
+ const skip = document.createElement("button");
759
+ skip.type = "button";
760
+ skip.textContent = "Skip";
761
+ skip.style.cssText = `padding:9px 16px;border-radius:999px;border:1px solid color-mix(in srgb, ${accent} 30%, transparent);background:transparent;color:${fg};font-weight:600;font-size:13px;cursor:pointer;`;
762
+ skip.addEventListener("click", () => finish(false));
763
+ btnRow.appendChild(send);
764
+ btnRow.appendChild(skip);
765
+ wrap.appendChild(btnRow);
766
+ ta.focus();
767
+ }
569
768
  function buildNpsElement(el, ctx, fg, accent, muted) {
570
769
  const wrap = document.createElement("div");
571
770
  wrap.style.cssText = "display:flex;flex-direction:column;gap:6px;margin:2px 0;";
@@ -601,10 +800,15 @@ function buildNpsElement(el, ctx, fg, accent, muted) {
601
800
  btn.style.background = accent;
602
801
  btn.style.color = "#fff";
603
802
  btn.style.opacity = "1";
604
- ctx.trackEvent(ctx.campaign.id, "clicked", { meta: { element: "nps", score: n } });
605
- void ((_a = ctx.submitResponse) == null ? void 0 : _a.call(ctx, "nps", { score: n }));
606
- (_b = ctx.playReaction) == null ? void 0 : _b.call(ctx, wrap, n >= 9 ? "affirm" : n >= 7 ? "acknowledge" : "empathize");
607
- appendThanks(wrap, el, fg, "Thanks for your feedback!");
803
+ const npsPayload = { score: n, band: npsBandFromScore(n) };
804
+ ctx.trackEvent(ctx.campaign.id, "clicked", { meta: { element: "nps", score: n, band: npsPayload.band } });
805
+ if (el.nps_followup_enabled === true) {
806
+ renderNpsReasonStep(wrap, el, ctx, n, fg, accent);
807
+ } else {
808
+ void ((_a = ctx.submitResponse) == null ? void 0 : _a.call(ctx, "nps", { ...npsPayload }));
809
+ (_b = ctx.playReaction) == null ? void 0 : _b.call(ctx, wrap, n >= 9 ? "affirm" : n >= 7 ? "acknowledge" : "empathize");
810
+ appendThanks(wrap, el, fg, "Thanks for your feedback!");
811
+ }
608
812
  });
609
813
  row.appendChild(btn);
610
814
  }
@@ -642,9 +846,8 @@ function buildProgressElement(el, ctx, accent, muted) {
642
846
  return 0;
643
847
  };
644
848
  const readCur = () => {
645
- const cart = ctx.getCartState && ctx.getCartState();
646
- if (goal === "items_in_cart") return Number(cart == null ? void 0 : cart.itemCount) || 0;
647
- if (goal === "cart_total") return Number(cart == null ? void 0 : cart.total) || 0;
849
+ const fromCart = readGoalFromCart(goal, ctx.getCartState && ctx.getCartState());
850
+ if (fromCart !== void 0) return fromCart;
648
851
  const p = props();
649
852
  if (goal === "loyalty_points") return Number(p["loyalty.points"]) || 0;
650
853
  if (goal === "referrals") return Number(p["referrals.count"]) || 0;
@@ -666,10 +869,18 @@ function buildProgressElement(el, ctx, accent, muted) {
666
869
  var _a;
667
870
  const th = threshold();
668
871
  const cur = readCur();
669
- const pct = th > 0 ? Math.max(0, Math.min(100, cur / th * 100)) : 0;
872
+ const { pct, complete } = computeProgress({ threshold: th, current: cur });
670
873
  bar.style.width = `${pct}%`;
671
- if (th > 0 && pct >= 100) {
672
- sub.textContent = el.reward_text || "Unlocked!";
874
+ const labeled = cardProgressSubLabel({
875
+ goalType: goal,
876
+ threshold: th,
877
+ current: cur,
878
+ rewardText: el.reward_text,
879
+ currencySymbol: moneyGoal ? sym() : "",
880
+ showPct: el.show_pct
881
+ });
882
+ sub.textContent = labeled.text;
883
+ if (complete) {
673
884
  sub.style.color = "#10b981";
674
885
  sub.style.fontWeight = "700";
675
886
  if (!firedUnlock) {
@@ -681,10 +892,6 @@ function buildProgressElement(el, ctx, accent, muted) {
681
892
  sub.style.color = muted;
682
893
  sub.style.fontWeight = "";
683
894
  firedUnlock = false;
684
- const remaining = Math.max(0, th - cur);
685
- const pre = moneyGoal ? sym() : "";
686
- const pctStr = el.show_pct && th > 0 ? ` · ${Math.round(pct)}%` : "";
687
- sub.textContent = th > 0 ? `Shop for ${pre}${remaining.toFixed(0)} more${pctStr}` : el.reward_text || "";
688
895
  }
689
896
  };
690
897
  update();
@@ -1011,6 +1218,9 @@ function renderCarouselCards(ctx) {
1011
1218
  layoutAccent: accent
1012
1219
  });
1013
1220
  tile.setAttribute("data-card-index", String(i));
1221
+ tile.addEventListener("click", () => {
1222
+ trackEvent(campaign.id, "clicked", { stepId: `card_${i}` });
1223
+ });
1014
1224
  track.appendChild(tile);
1015
1225
  });
1016
1226
  card.appendChild(track);
@@ -1311,21 +1521,23 @@ function buildProgressBar(ctx, inline) {
1311
1521
  };
1312
1522
  const update = () => {
1313
1523
  const cur = readCurrentValue();
1314
- const pct = Math.max(0, Math.min(100, cur / threshold * 100));
1524
+ const { pct, complete, remaining, milestonesReached } = computeProgress({
1525
+ threshold,
1526
+ current: cur
1527
+ });
1315
1528
  fillEl.style.width = `${pct}%`;
1316
1529
  numeric.textContent = `${cur.toFixed(0)} / ${threshold.toFixed(0)} · ${Math.round(pct)}%`;
1317
- shimmer.style.display = pct >= 100 ? "none" : "";
1318
- if (pct >= 25) fireMilestone("q25");
1319
- if (pct >= 50) fireMilestone("q50");
1320
- if (pct >= 75) fireMilestone("q75");
1321
- if (pct >= 100) {
1530
+ shimmer.style.display = complete ? "none" : "";
1531
+ for (const m2 of milestonesReached) {
1532
+ if (m2 !== "unlocked") fireMilestone(m2);
1533
+ }
1534
+ if (complete) {
1322
1535
  footnote.textContent = rewardText;
1323
1536
  if (!lastFiredUnlock) {
1324
1537
  lastFiredUnlock = true;
1325
1538
  trackEvent(campaign.id, "clicked", { stepId: "unlocked" });
1326
1539
  }
1327
1540
  } else {
1328
- const remaining = Math.max(0, threshold - cur);
1329
1541
  footnote.textContent = `Add ${remaining.toFixed(0)} more to unlock: ${rewardText}`;
1330
1542
  }
1331
1543
  };
@@ -2244,6 +2456,9 @@ function injectStoryStyles() {
2244
2456
  `;
2245
2457
  document.head.appendChild(style);
2246
2458
  }
2459
+ function earnPct(current, threshold) {
2460
+ return computeProgress({ threshold, current }).pct;
2461
+ }
2247
2462
  const num = (v, d) => {
2248
2463
  const n = Number(v);
2249
2464
  return Number.isFinite(n) ? n : d;
@@ -2286,7 +2501,8 @@ function openGameModal(ctx, className, maxWidth = 360) {
2286
2501
  };
2287
2502
  const close = document.createElement("button");
2288
2503
  close.type = "button";
2289
- close.innerHTML = lucideSvg("x", { size: 15 });
2504
+ const closeIcon = lucideSvgEl("x", { size: 15 });
2505
+ if (closeIcon) close.appendChild(closeIcon);
2290
2506
  close.setAttribute("aria-label", "Close");
2291
2507
  close.style.cssText = `position:absolute;top:10px;right:10px;width:30px;height:30px;border:none;border-radius:999px;background:${branded ? "rgba(255,255,255,0.18)" : "rgba(0,0,0,0.05)"};color:${branded ? "#fff" : "#475569"};font-size:15px;cursor:pointer;z-index:1;`;
2292
2508
  close.addEventListener("click", () => dismiss(true));
@@ -2378,7 +2594,7 @@ function renderMilestone(ctx) {
2378
2594
  const cur = num(ic.milestone_current, 1200);
2379
2595
  const target = num(ic.milestone_target, 2e3);
2380
2596
  const steps = arr(ic.milestone_steps);
2381
- const pct = target > 0 ? cur / target * 100 : 0;
2597
+ const pct = earnPct(cur, target);
2382
2598
  s.content.appendChild(gEl("div", "font-size:13px;opacity:0.8;margin-bottom:2px", `${unit}${cur} of ${unit}${target}`));
2383
2599
  s.content.appendChild(fillBar(s.accent, pct));
2384
2600
  s.content.appendChild(gEl("div", "font-size:13px;font-weight:600", `${unit}${Math.max(0, target - cur)} to go`));
@@ -2399,7 +2615,7 @@ function renderChallenge(ctx) {
2399
2615
  const s = openGameModal(ctx, "aegis-in-app-challenge");
2400
2616
  const tasks = arr(ic.challenge_tasks);
2401
2617
  const done = tasks.filter((t) => t.done).length;
2402
- const pct = tasks.length ? done / tasks.length * 100 : 0;
2618
+ const pct = tasks.length ? earnPct(done, tasks.length) : 0;
2403
2619
  if (str$1(ic.challenge_deadline_text)) s.content.appendChild(gEl("div", "font-size:12px;opacity:0.65;margin-bottom:8px", str$1(ic.challenge_deadline_text)));
2404
2620
  tasks.forEach((t) => {
2405
2621
  const row = gEl("div", "display:flex;align-items:center;gap:8px;padding:7px 0;border-bottom:1px solid rgba(148,163,184,0.18);font-size:13px;text-align:left");
@@ -2610,7 +2826,7 @@ function renderStampCard(ctx) {
2610
2826
  const nextReward = [...rewardAt.keys()].filter((p) => p > filled).sort((a, b) => a - b)[0];
2611
2827
  const progressTxt = nextReward ? `${nextReward - filled} more → ${rewardAt.get(nextReward)}` : "Card complete!";
2612
2828
  const sub = gEl("div", "font-size:13px;font-weight:600;margin-top:2px");
2613
- sub.appendChild(fillBar(ink, filled / total * 100));
2829
+ sub.appendChild(fillBar(ink, earnPct(filled, total)));
2614
2830
  sub.appendChild(gEl("div", "", `${filled} of ${total}`));
2615
2831
  sub.appendChild(gEl("div", "font-weight:400;opacity:0.82;margin-top:1px", str$1(ic.stamp_reward_text) || progressTxt));
2616
2832
  s.content.appendChild(sub);
@@ -2713,7 +2929,7 @@ function renderTierLadder(ctx) {
2713
2929
  const curIdx = Math.min(list.length - 1, num(ic.tier_current_index, 1));
2714
2930
  const curPts = num(ic.tier_current_points, 640);
2715
2931
  const nextPts = num(ic.tier_next_points, 1e3);
2716
- const pct = nextPts > 0 ? curPts / nextPts * 100 : 0;
2932
+ const pct = earnPct(curPts, nextPts);
2717
2933
  const perks = arr(ic.tier_perks);
2718
2934
  const next = list[Math.min(list.length - 1, curIdx + 1)];
2719
2935
  const ladder = gEl("div", "display:flex;align-items:flex-start;justify-content:center;margin:6px 0 12px");
@@ -4565,6 +4781,13 @@ let activeTriggers = null;
4565
4781
  function str(v) {
4566
4782
  return typeof v === "string" && v.length > 0 ? v : void 0;
4567
4783
  }
4784
+ function resolveChatDisplayMode(raw) {
4785
+ var _a, _b;
4786
+ if (raw === "nav") return "nav";
4787
+ if (raw === "bubble") return "bubble";
4788
+ const isMobile = typeof window !== "undefined" && ((_b = (_a = window.matchMedia) == null ? void 0 : _a.call(window, "(max-width: 767px)")) == null ? void 0 : _b.matches) === true;
4789
+ return isMobile ? "nav" : "bubble";
4790
+ }
4568
4791
  function renderActiveWebChat(ctx, creds) {
4569
4792
  if (typeof document === "undefined") return null;
4570
4793
  if (activeInstance) return activeInstance;
@@ -4584,10 +4807,9 @@ function renderActiveWebChat(ctx, creds) {
4584
4807
  logoUrl: logo ? ctx.sanitizeUrl(logo) ?? void 0 : void 0,
4585
4808
  position: ic.chat_position === "bottom-left" ? "bottom-left" : void 0,
4586
4809
  surfaceStyle: ic.chat_theme === "tinted" ? "tinted" : void 0,
4587
- // 'nav' = headless (no bubble); the host opens it via aegis.chat.open() or a
4588
- // client_trigger. Defaults to 'bubble'. Per-device override via device_type
4589
- // targeting on the campaign (e.g. a mobile campaign with chat_display_mode='nav').
4590
- displayMode: ic.chat_display_mode === "nav" ? "nav" : void 0,
4810
+ // Explicit chat_display_mode wins; unset → responsive (phones = headless
4811
+ // 'nav' so the host's bottom-nav "Ask" tab is the entry; desktop = 'bubble').
4812
+ displayMode: resolveChatDisplayMode(ic.chat_display_mode),
4591
4813
  agentPersona: str(ic.chat_agent_persona),
4592
4814
  quickReplies: Array.isArray(ic.chat_quick_replies) ? ic.chat_quick_replies.filter((q) => typeof q === "string") : void 0,
4593
4815
  // Bridge the engine's session lifecycle to campaign analytics. Per the
@@ -5107,6 +5329,7 @@ const _AegisInAppManager = class _AegisInAppManager {
5107
5329
  this.emitServedEvents(this.campaigns);
5108
5330
  this.emit("campaigns-loaded", this.campaigns);
5109
5331
  this.readyResolve();
5332
+ this.mountPersistentLaunchers();
5110
5333
  this.tryDisplayNextCampaign();
5111
5334
  this.renderIntoSlots();
5112
5335
  this.renderIntoAnchors();
@@ -5170,12 +5393,28 @@ const _AegisInAppManager = class _AegisInAppManager {
5170
5393
  }
5171
5394
  tryDisplayNextCampaign() {
5172
5395
  const campaign = this.campaigns.find(
5173
- (c) => !this.displayedCampaigns.has(c.id) && !c.client_trigger && !this.isSuppressed(c.id)
5396
+ (c) => !this.displayedCampaigns.has(c.id) && !c.client_trigger && !this.isSuppressed(c.id) && c.type !== "active_web_chat"
5174
5397
  );
5175
5398
  if (campaign) {
5176
5399
  this.displayCampaign(campaign);
5177
5400
  }
5178
5401
  }
5402
+ /** Persistent launchers (active_web_chat) mount AMBIENTLY on every arm — a
5403
+ * single always-on entry point, NOT an interruption, so they must not
5404
+ * compete in the one-at-a-time overlay queue (tryDisplayNextCampaign).
5405
+ * Idempotent: renderActiveWebChatLauncher no-ops once a launcher is live and
5406
+ * displayedCampaigns guards re-display. Before this, the launcher only
5407
+ * mounted when it happened to be the single campaign tryDisplayNextCampaign
5408
+ * picked — so aegis.chat.open() / the mobile "Ask" tab silently did nothing
5409
+ * whenever another non-triggered campaign was armed first. */
5410
+ mountPersistentLaunchers() {
5411
+ for (const c of this.campaigns) {
5412
+ if (c.type !== "active_web_chat") continue;
5413
+ if (this.displayedCampaigns.has(c.id)) continue;
5414
+ if (this.isSuppressed(c.id)) continue;
5415
+ this.displayCampaign(c);
5416
+ }
5417
+ }
5179
5418
  /**
5180
5419
  * Scan the page for `[data-aegis-slot]` anchors and render any
5181
5420
  * campaign whose `widget_category` matches the slot key AND whose
@@ -5556,7 +5795,6 @@ const _AegisInAppManager = class _AegisInAppManager {
5556
5795
  target = sizer;
5557
5796
  }
5558
5797
  const { bg, text } = this._surfacePalette(campaign);
5559
- const submitUrl = options == null ? void 0 : options.submitUrl;
5560
5798
  const _formFields = Array.isArray(ic.form_fields) ? ic.form_fields : null;
5561
5799
  let rendered = false;
5562
5800
  if (_formFields && _formFields.length > 0) {
@@ -5571,43 +5809,22 @@ const _AegisInAppManager = class _AegisInAppManager {
5571
5809
  }
5572
5810
  return;
5573
5811
  }
5574
- const _tpl = ic.card_template;
5575
- const _hasComposedTemplate = !!_tpl && typeof _tpl === "object" && Array.isArray(_tpl.elements);
5812
+ this._ensureSurveyTemplate(campaign);
5576
5813
  const _isMultiCard = campaign.sub_type === "product_recommendation" || campaign.sub_type === "carousel_cards";
5577
- if (_hasComposedTemplate && !_isMultiCard) {
5814
+ if (this._hasComposedTemplate(ic) && !_isMultiCard) {
5578
5815
  rendered = this.renderComposedCardSlot(campaign, ic, target);
5579
5816
  } else if (campaign.widget_category === "hero") {
5580
5817
  rendered = renderHeroInline(this.buildRenderContext(campaign), target);
5581
5818
  } else
5582
5819
  switch (campaign.sub_type) {
5583
- case "star_rating":
5584
- rendered = this.renderStarRatingSlot(
5585
- campaign,
5586
- ic,
5587
- bg,
5588
- text,
5589
- target,
5590
- submitUrl
5591
- );
5592
- break;
5593
- case "nps_survey":
5594
- rendered = this.renderNPSSurveySlot(
5595
- campaign,
5596
- ic,
5597
- bg,
5598
- text,
5599
- target,
5600
- submitUrl
5601
- );
5602
- break;
5820
+ // P6.full — star_rating / nps_survey / quick_poll slots route through the
5821
+ // unified composed-card slot engine via `_ensureSurveyTemplate` (the gate
5822
+ // above), so the bespoke *Slot survey renderers are RETIRED.
5603
5823
  // Parity tracker — built-in embedded gamification renderers (no
5604
5824
  // AegisMessageRuntime callback needed, so they work on the bill).
5605
5825
  case "spin_wheel":
5606
5826
  rendered = this.renderSpinWheelSlot(campaign, ic, bg, text, target);
5607
5827
  break;
5608
- case "quick_poll":
5609
- rendered = this.renderQuickPollSlot(campaign, ic, bg, text, target);
5610
- break;
5611
5828
  case "carousel_cards":
5612
5829
  rendered = this.renderCarouselCardsSlot(campaign, ic, bg, text, target);
5613
5830
  break;
@@ -5652,48 +5869,70 @@ const _AegisInAppManager = class _AegisInAppManager {
5652
5869
  this.emit("campaign-shown", campaign);
5653
5870
  }
5654
5871
  }
5655
- /**
5656
- * Slot-mode wrappers. They build a small card container (no overlay
5657
- * positioning, no close button) and append the SHARED body element
5658
- * the overlay path also uses. Body construction lives in the
5659
- * `_buildXxxBody` helpers below so visual output stays identical
5660
- * between overlay and slot modes — adding a renderer feature in
5661
- * one place updates both.
5662
- */
5663
- renderStarRatingSlot(campaign, ic, bg, text, target, submitUrl) {
5664
- const st = this._slotStyle(campaign);
5665
- const card = this._wrapInSlotCard(
5666
- "aegis-in-app-rating-card",
5667
- campaign.id,
5668
- bg,
5669
- text,
5670
- st.radius
5671
- );
5672
- card.appendChild(
5673
- this._buildStarRatingBody(campaign, ic, text, "slot", submitUrl, st)
5674
- );
5675
- target.appendChild(card);
5676
- return true;
5677
- }
5678
- renderNPSSurveySlot(campaign, ic, bg, text, target, submitUrl) {
5679
- const st = this._slotStyle(campaign);
5680
- const card = this._wrapInSlotCard(
5681
- "aegis-in-app-nps-card",
5682
- campaign.id,
5683
- bg,
5684
- text,
5685
- st.radius
5686
- );
5687
- card.appendChild(
5688
- this._buildNPSSurveyBody(campaign, ic, text, "slot", submitUrl, st)
5689
- );
5690
- target.appendChild(card);
5691
- return true;
5692
- }
5693
5872
  /** §7.13.H — generic embedded card: image / title / body / coupon chip / CTA.
5694
5873
  * The catch-all for banner + nudge sub_types (birthday, referral, app-install,
5695
5874
  * win-back, etc.) so EVERY gallery nudge renders on the bill (previously only
5696
5875
  * star_rating / nps_survey / form were handled → nudges never appeared). */
5876
+ /**
5877
+ * Whether `interactive_config` carries a composed `card_template` (non-empty
5878
+ * `elements[]`). The ONE detector shared by all three composed-card cutover
5879
+ * gates — inline slot, chrome overlay, and the P6 survey-overlay cutover — so
5880
+ * they can't drift (the inline gate previously omitted the non-empty check).
5881
+ */
5882
+ _hasComposedTemplate(ic) {
5883
+ const tpl = ic.card_template;
5884
+ return !!tpl && typeof tpl === "object" && Array.isArray(tpl.elements) && tpl.elements.length > 0;
5885
+ }
5886
+ /**
5887
+ * P6.full — flat survey → card_template adapter. A `nps_survey` / `star_rating`
5888
+ * / `quick_poll` authored WITHOUT a card_template gets one synthesised in place
5889
+ * (carrying its follow-up config), so the EXISTING composed-template gates route
5890
+ * it through the ONE card-template engine + the shared element builders (with
5891
+ * the ported reason-capture/thank-you flows) — overlay AND slot. Back-compat:
5892
+ * the field names + follow-up defaults match the retired bespoke renderers.
5893
+ * No-op when a template already exists or the type isn't a single-card survey.
5894
+ */
5895
+ _ensureSurveyTemplate(campaign) {
5896
+ const ic = campaign.interactive_config || {};
5897
+ if (this._hasComposedTemplate(ic)) return;
5898
+ let element = null;
5899
+ if (campaign.sub_type === "nps_survey") {
5900
+ element = {
5901
+ type: "nps",
5902
+ content: ic.question,
5903
+ thanks_text: ic.thank_you_message,
5904
+ nps_followup_enabled: ic.nps_followup_enabled !== false,
5905
+ nps_follow_up_promoter: ic.nps_follow_up_promoter,
5906
+ nps_follow_up_passive: ic.nps_follow_up_passive,
5907
+ nps_follow_up_detractor: ic.nps_follow_up_detractor,
5908
+ nps_reason_chips: ic.nps_reason_chips,
5909
+ nps_promoter_threshold: ic.nps_promoter_threshold,
5910
+ nps_promoter_url: ic.nps_promoter_url,
5911
+ nps_promoter_cta: ic.nps_promoter_cta
5912
+ };
5913
+ } else if (campaign.sub_type === "star_rating") {
5914
+ element = {
5915
+ type: "rating_input",
5916
+ content: ic.question,
5917
+ rating_scale: ic.rating_scale,
5918
+ thanks_text: ic.thank_you_message,
5919
+ rating_followup_enabled: ic.rating_followup_enabled !== false,
5920
+ rating_followup_threshold: ic.rating_followup_threshold,
5921
+ rating_followup_prompt: ic.rating_followup_prompt,
5922
+ rating_high_url: ic.rating_high_url ?? ic.nps_promoter_url,
5923
+ rating_high_cta: ic.rating_high_cta
5924
+ };
5925
+ } else if (campaign.sub_type === "quick_poll") {
5926
+ const opts = Array.isArray(ic.poll_options) ? ic.poll_options : Array.isArray(ic.options) ? ic.options : [];
5927
+ if (opts.length >= 2) {
5928
+ element = { type: "poll", content: ic.question, poll_options: opts, thanks_text: ic.thank_you_message };
5929
+ }
5930
+ }
5931
+ if (!element) return;
5932
+ const cleaned = Object.fromEntries(Object.entries(element).filter(([, v]) => v !== void 0));
5933
+ ic.card_template = { frame: {}, elements: [cleaned] };
5934
+ campaign.interactive_config = ic;
5935
+ }
5697
5936
  /**
5698
5937
  * Composed single card — renders a campaign's top-level `card_template`
5699
5938
  * (frame + elements: icon/text/progress/image/cta/group/…) through the generic
@@ -6646,88 +6885,6 @@ const _AegisInAppManager = class _AegisInAppManager {
6646
6885
  }
6647
6886
  return true;
6648
6887
  }
6649
- /** Built-in embedded QUICK POLL — option buttons submit the chosen index. */
6650
- renderQuickPollSlot(campaign, ic, bg, text, target) {
6651
- const st = this._slotStyle(campaign);
6652
- const card = this._wrapInSlotCard("aegis-in-app-poll-card", campaign.id, bg, text, st.radius);
6653
- const opts = Array.isArray(ic.poll_options) ? ic.poll_options.filter((o) => typeof o === "string") : [];
6654
- const body = document.createElement("div");
6655
- body.style.cssText = `padding: ${st.padY ?? 12}px ${st.padX ?? 12}px; display: flex; flex-direction: column; gap: 6px;`;
6656
- if (campaign.title) {
6657
- const t = document.createElement("div");
6658
- t.style.cssText = `font-size: 13px; font-weight: ${st.weight ?? "700"};`;
6659
- t.textContent = campaign.title;
6660
- body.appendChild(t);
6661
- }
6662
- const buttons = [];
6663
- opts.forEach((opt, i) => {
6664
- const ob = document.createElement("button");
6665
- ob.style.cssText = `position: relative; overflow: hidden; text-align: left; padding: 8px 10px; border-radius: ${st.btnRadius ?? 8}px; border: 1px solid ${text}40; background: transparent; color: ${text}; font-size: 12px; cursor: pointer; transition: border-color 0.2s ease;`;
6666
- const lbl = document.createElement("span");
6667
- lbl.style.cssText = "position: relative; z-index: 1; display: flex; justify-content: space-between; gap: 8px;";
6668
- const lblText = document.createElement("span");
6669
- lblText.textContent = opt;
6670
- const lblPct = document.createElement("span");
6671
- lblPct.style.cssText = "font-weight: 700; opacity: 0; transition: opacity 0.3s ease;";
6672
- lbl.appendChild(lblText);
6673
- lbl.appendChild(lblPct);
6674
- ob.appendChild(lbl);
6675
- ob.addEventListener("click", () => {
6676
- void this.trackEvent(campaign.id, "clicked");
6677
- buttons.forEach((b) => b.disabled = true);
6678
- this._paintPollResult(buttons, opts, i, null, text);
6679
- this.addAnimationStyles();
6680
- void this.submitWidgetResponse(campaign, "poll", { option_index: i, option_label: opt }).then((resp) => {
6681
- const dist = this._readPollDistribution(resp, opts.length);
6682
- if (dist) this._paintPollResult(buttons, opts, i, dist, text);
6683
- });
6684
- this._playReaction(card, "affirm", "check.json", ic);
6685
- });
6686
- buttons.push(ob);
6687
- body.appendChild(ob);
6688
- });
6689
- card.appendChild(body);
6690
- target.appendChild(card);
6691
- return true;
6692
- }
6693
- /** Pull a poll vote distribution out of the server response, if present.
6694
- * Accepts `results`/`distribution`/`tallies` as a number[] aligned to the
6695
- * option order. Returns null when the server didn't surface tallies (then
6696
- * we only confirm the chosen option — never fabricate percentages). */
6697
- _readPollDistribution(resp, count) {
6698
- if (!resp) return null;
6699
- const raw = resp.results ?? resp.distribution ?? resp.tallies;
6700
- if (!Array.isArray(raw) || raw.length !== count) return null;
6701
- const nums = raw.map((v) => typeof v === "number" && isFinite(v) && v >= 0 ? v : 0);
6702
- return nums.some((n) => n > 0) ? nums : null;
6703
- }
6704
- /** Animate result bars on the poll options. With a real `dist` it renders
6705
- * each option's share; without one it just fills the selected option 100%
6706
- * as a "vote locked in" confirmation. */
6707
- _paintPollResult(buttons, _opts, selected, dist, text) {
6708
- const total = dist ? dist.reduce((a, b) => a + b, 0) || 1 : 1;
6709
- buttons.forEach((ob, i) => {
6710
- const pct = dist ? Math.round(dist[i] / total * 100) : i === selected ? 100 : 0;
6711
- let fill = ob.querySelector(".aegis-poll-fill");
6712
- if (!fill) {
6713
- fill = document.createElement("span");
6714
- fill.className = "aegis-poll-fill";
6715
- fill.style.cssText = `position: absolute; inset: 0 auto 0 0; z-index: 0; background: ${i === selected ? text : text + "22"}; opacity: ${i === selected ? "0.16" : "0.1"};`;
6716
- ob.insertBefore(fill, ob.firstChild);
6717
- }
6718
- fill.style.setProperty("--pct", `${pct}%`);
6719
- fill.style.animation = "aegisPollFill 0.5s ease forwards";
6720
- if (i === selected) ob.style.borderColor = text;
6721
- const pctLabel = ob.querySelector("span > span:last-child");
6722
- if (pctLabel && dist) {
6723
- pctLabel.textContent = `${pct}%`;
6724
- pctLabel.style.opacity = "0.8";
6725
- } else if (pctLabel && i === selected) {
6726
- pctLabel.innerHTML = lucideSvg("check", { size: 14 });
6727
- pctLabel.style.opacity = "0.8";
6728
- }
6729
- });
6730
- }
6731
6888
  /**
6732
6889
  * Built-in embedded IMAGE/VIDEO CAROUSEL — a swipeable horizontal strip of
6733
6890
  * `interactive_config.cards` (image OR video + title + body + CTA), contained
@@ -7306,23 +7463,6 @@ const _AegisInAppManager = class _AegisInAppManager {
7306
7463
  for (const c of cands) if (typeof c === "string" && c) return c;
7307
7464
  return null;
7308
7465
  }
7309
- /**
7310
- * Token-bound submission helper. POSTs the structured response to
7311
- * the per-token submit URL the page handler embedded. Best-effort
7312
- * — failure is logged but doesn't throw (the customer already
7313
- * clicked; we can't undo their interaction).
7314
- */
7315
- _submitTokenResponse(submitUrl, payload) {
7316
- if (!submitUrl) return;
7317
- fetch(submitUrl, {
7318
- method: "POST",
7319
- headers: { "Content-Type": "application/json" },
7320
- body: JSON.stringify({ response: payload }),
7321
- keepalive: true
7322
- }).catch((err) => {
7323
- this.log(`token submit failed: ${err}`, "warn");
7324
- });
7325
- }
7326
7466
  // --- Shared body builders (used by both overlay + slot paths) ----------
7327
7467
  //
7328
7468
  // Sizing: `'overlay'` matches the legacy fullscreen-modal scale (24px
@@ -7383,398 +7523,6 @@ const _AegisInAppManager = class _AegisInAppManager {
7383
7523
  el.style.boxShadow = "none";
7384
7524
  }
7385
7525
  }
7386
- // Optional header/brand image atop a survey-style widget (star / nps).
7387
- _appendHeaderImage(body, ic) {
7388
- const url = ic.header_image_url;
7389
- if (!url) return;
7390
- const safe = this.sanitizeUrl(url);
7391
- if (!safe) return;
7392
- const img = document.createElement("img");
7393
- img.src = safe;
7394
- img.alt = "";
7395
- img.style.cssText = "display: block; max-height: 72px; max-width: 60%; margin: 0 auto 14px; object-fit: contain;";
7396
- body.appendChild(img);
7397
- }
7398
- _buildStarRatingBody(campaign, ic, text, variant, submitUrl, slotStyle) {
7399
- const isOverlay = variant === "overlay";
7400
- const padding = isOverlay ? "24px" : `${(slotStyle == null ? void 0 : slotStyle.padY) ?? 20}px ${(slotStyle == null ? void 0 : slotStyle.padX) ?? 20}px`;
7401
- const titleSize = isOverlay ? "18px" : "16px";
7402
- const titleWeight = isOverlay ? "700" : (slotStyle == null ? void 0 : slotStyle.weight) ?? "600";
7403
- const titleMargin = isOverlay ? "16px" : "12px";
7404
- const starSize = isOverlay ? "32px" : "28px";
7405
- const starsMarginBottom = isOverlay ? "16px" : "0";
7406
- const hoverScale = isOverlay ? "1.2" : "1.15";
7407
- const body = document.createElement("div");
7408
- body.style.cssText = `padding: ${padding}; text-align: center;`;
7409
- this._appendHeaderImage(body, ic);
7410
- const title = document.createElement("div");
7411
- title.style.cssText = `font-size: ${titleSize}; font-weight: ${titleWeight}; margin-bottom: ${titleMargin};`;
7412
- title.textContent = campaign.title || "Rate your experience";
7413
- body.appendChild(title);
7414
- const stars = document.createElement("div");
7415
- stars.style.cssText = `display: flex; gap: 8px; justify-content: center;` + (starsMarginBottom !== "0" ? ` margin-bottom: ${starsMarginBottom};` : "");
7416
- const maxStars = ic.rating_scale || 5;
7417
- const GOLD = "#fbbf24";
7418
- const iconSet = ic.rating_icon || "star";
7419
- const EMOJI = ["😞", "🙁", "😐", "🙂", "😀", "🤩"];
7420
- const emojiFor = (idx) => {
7421
- const k = maxStars <= 1 ? EMOJI.length - 1 : Math.round(idx / (maxStars - 1) * (EMOJI.length - 1));
7422
- return EMOJI[Math.max(0, Math.min(EMOJI.length - 1, k))];
7423
- };
7424
- const glyph = (on, idx) => iconSet === "emoji" ? emojiFor(idx) : iconSet === "heart" ? on ? "♥" : "♡" : on ? "★" : "☆";
7425
- const labels = Array.isArray(ic.rating_labels) ? ic.rating_labels : null;
7426
- let caption2 = null;
7427
- if (labels && labels.length > 0) {
7428
- caption2 = document.createElement("div");
7429
- caption2.style.cssText = `font-size: 12px; opacity: 0.7; margin-top: 8px; min-height: 16px; color: ${text || "inherit"};`;
7430
- }
7431
- const starEls = [];
7432
- let selected = 0;
7433
- const paint = (n) => {
7434
- starEls.forEach((el, idx) => {
7435
- const on = idx < n;
7436
- el.textContent = glyph(on, idx);
7437
- if (iconSet === "emoji") {
7438
- el.style.opacity = on || n === 0 ? "1" : "0.3";
7439
- el.style.color = "inherit";
7440
- } else {
7441
- el.style.color = on ? GOLD : text || "inherit";
7442
- }
7443
- el.style.transform = on && idx === n - 1 ? `scale(${hoverScale})` : "scale(1)";
7444
- });
7445
- if (caption2 && labels) {
7446
- const lbl = n > 0 ? labels[Math.min(n - 1, labels.length - 1)] : "";
7447
- caption2.textContent = lbl || "";
7448
- }
7449
- };
7450
- for (let i = 1; i <= maxStars; i++) {
7451
- const star = document.createElement("span");
7452
- star.style.cssText = `font-size: ${starSize}; cursor: pointer; transition: transform 0.12s, color 0.12s, opacity 0.12s; user-select: none;` + (iconSet !== "emoji" && text ? ` color: ${text};` : "") + (iconSet === "emoji" ? " opacity: 0.3;" : "");
7453
- star.setAttribute("role", "button");
7454
- star.setAttribute("aria-label", `${i} ${iconSet === "heart" ? "heart" : "star"}${i > 1 ? "s" : ""}`);
7455
- star.textContent = glyph(false, i - 1);
7456
- const value = i;
7457
- star.addEventListener("mouseenter", () => paint(value));
7458
- star.addEventListener("mouseleave", () => paint(selected));
7459
- star.addEventListener("click", () => {
7460
- selected = value;
7461
- paint(selected);
7462
- star.style.transform = "scale(1.28)";
7463
- setTimeout(() => paint(selected), 160);
7464
- this.trackEvent(campaign.id, "clicked");
7465
- this._onStarRatingPicked(campaign, ic, body, value, maxStars, text, isOverlay, submitUrl);
7466
- });
7467
- starEls.push(star);
7468
- stars.appendChild(star);
7469
- }
7470
- body.appendChild(stars);
7471
- if (caption2) body.appendChild(caption2);
7472
- return body;
7473
- }
7474
- /**
7475
- * Post-rating flow — the Reelo/Plotline "smart rating" gate. A happy rater
7476
- * (>= threshold) is invited to leave a PUBLIC review; an unhappy one is given
7477
- * a PRIVATE "what went wrong?" comment box so the complaint is captured for
7478
- * the operator instead of landing on a public platform. Exactly ONE response
7479
- * row is written per rating (deferred on the low path until Send/Skip) so the
7480
- * star distribution isn't double-counted.
7481
- */
7482
- _onStarRatingPicked(campaign, ic, body, value, maxStars, text, isOverlay, submitUrl) {
7483
- if (submitUrl) {
7484
- this._submitTokenResponse(submitUrl, { sub_type: "star_rating", value });
7485
- }
7486
- const followupEnabled = isOverlay && ic.rating_followup_enabled !== false;
7487
- const threshold = ic.rating_followup_threshold || Math.ceil(maxStars * 0.8);
7488
- const isHigh = value >= threshold;
7489
- if (!isOverlay) {
7490
- if (isHigh) this._playReaction(body, "affirm", void 0, ic);
7491
- else if (value <= Math.max(1, Math.floor(maxStars * 0.4))) this._playReaction(body, "empathize", void 0, ic);
7492
- else this._playReaction(body, "acknowledge", void 0, ic);
7493
- return;
7494
- }
7495
- if (!followupEnabled) {
7496
- void this.submitWidgetResponse(campaign, "rating", { stars: value });
7497
- this._renderRatingThankYou(campaign, ic, body, isHigh);
7498
- return;
7499
- }
7500
- if (isHigh) {
7501
- void this.submitWidgetResponse(campaign, "rating", { stars: value });
7502
- this._renderRatingThankYou(campaign, ic, body, true);
7503
- } else {
7504
- this._renderRatingLowFollowup(campaign, ic, body, value, text);
7505
- }
7506
- }
7507
- /** Thank-you takeover; happy raters also get a public-review CTA when authored. */
7508
- _renderRatingThankYou(campaign, ic, body, isHigh) {
7509
- while (body.firstChild) body.removeChild(body.firstChild);
7510
- const modalEl = body.parentElement;
7511
- if (modalEl) modalEl.style.overflow = "visible";
7512
- body.style.position = "relative";
7513
- body.style.minHeight = "176px";
7514
- body.style.display = "flex";
7515
- body.style.flexDirection = "column";
7516
- body.style.alignItems = "center";
7517
- body.style.justifyContent = "center";
7518
- const heading = document.createElement("div");
7519
- heading.style.cssText = "font-size: 16px; font-weight: 700; margin-bottom: 8px;";
7520
- heading.textContent = isHigh ? ic.rating_high_prompt || "Thanks — glad you loved it!" : ic.thank_you_message || "Thanks for your feedback!";
7521
- body.appendChild(heading);
7522
- const reviewUrl = ic.rating_review_url;
7523
- if (isHigh && reviewUrl) {
7524
- const sub = document.createElement("div");
7525
- sub.style.cssText = "font-size: 13px; opacity: 0.8; margin-bottom: 14px;";
7526
- sub.textContent = "Would you share that with others?";
7527
- body.appendChild(sub);
7528
- const st = this.inAppStyle(campaign);
7529
- const cta = document.createElement("button");
7530
- cta.type = "button";
7531
- cta.textContent = campaign.button_text || "Leave a review";
7532
- cta.style.cssText = `padding: 10px 24px; border-radius: 999px; border: none; background: ${st.btnBg}; color: ${st.btnText}; font-weight: 800; font-size: 13.5px; cursor: pointer;`;
7533
- cta.addEventListener("click", () => {
7534
- this.trackEvent(campaign.id, "clicked", { meta: { action: "review_redirect" } });
7535
- const safe = this.sanitizeUrl(reviewUrl);
7536
- if (safe) this.navigateToCampaignAction(campaign, safe);
7537
- });
7538
- body.appendChild(cta);
7539
- } else {
7540
- this._playReaction(body, isHigh ? "affirm" : "acknowledge", void 0, ic);
7541
- }
7542
- }
7543
- /** Unhappy-path private comment capture; submits {stars, comment} once. */
7544
- _renderRatingLowFollowup(campaign, ic, body, value, text) {
7545
- this.addAnimationStyles();
7546
- while (body.firstChild) body.removeChild(body.firstChild);
7547
- const heading = document.createElement("div");
7548
- heading.style.cssText = "font-size: 15px; font-weight: 700; margin-bottom: 10px;";
7549
- heading.textContent = ic.rating_low_prompt || "Sorry to hear that. What went wrong?";
7550
- body.appendChild(heading);
7551
- const ta = document.createElement("textarea");
7552
- ta.className = "aegis-rating-comment";
7553
- ta.rows = 3;
7554
- ta.placeholder = "Tell us how we can do better…";
7555
- ta.style.cssText = `width: 100%; box-sizing: border-box; padding: 10px; border-radius: 10px; border: 1px solid ${text}33; background: transparent; color: ${text || "inherit"}; font-size: 13px; resize: vertical; margin-bottom: 12px; font-family: inherit;`;
7556
- body.appendChild(ta);
7557
- const st = this.inAppStyle(campaign);
7558
- const row = document.createElement("div");
7559
- row.style.cssText = "display: flex; gap: 8px; justify-content: center;";
7560
- let done = false;
7561
- const finish = (withComment) => {
7562
- if (done) return;
7563
- done = true;
7564
- const comment = withComment ? ta.value.trim() : "";
7565
- void this.submitWidgetResponse(campaign, "rating", comment ? { stars: value, comment } : { stars: value });
7566
- this._renderRatingThankYou(campaign, ic, body, false);
7567
- };
7568
- const send = document.createElement("button");
7569
- send.type = "button";
7570
- send.textContent = "Send feedback";
7571
- send.style.cssText = `padding: 9px 20px; border-radius: 999px; border: none; background: ${st.btnBg}; color: ${st.btnText}; font-weight: 800; font-size: 13px; cursor: pointer;`;
7572
- send.addEventListener("click", () => finish(true));
7573
- const skip = document.createElement("button");
7574
- skip.type = "button";
7575
- skip.textContent = "Skip";
7576
- skip.style.cssText = `padding: 9px 16px; border-radius: 999px; border: 1px solid ${text}33; background: transparent; color: ${text || "inherit"}; font-weight: 600; font-size: 13px; cursor: pointer;`;
7577
- skip.addEventListener("click", () => finish(false));
7578
- row.appendChild(send);
7579
- row.appendChild(skip);
7580
- body.appendChild(row);
7581
- ta.focus();
7582
- }
7583
- _buildNPSSurveyBody(campaign, ic, text, variant, submitUrl, slotStyle) {
7584
- const isOverlay = variant === "overlay";
7585
- const padding = isOverlay ? "24px" : `${(slotStyle == null ? void 0 : slotStyle.padY) ?? 20}px ${(slotStyle == null ? void 0 : slotStyle.padX) ?? 20}px`;
7586
- const titleAlign = isOverlay ? "center" : "center";
7587
- const body = document.createElement("div");
7588
- body.style.cssText = `padding: ${padding};` + (isOverlay ? " text-align: center;" : "");
7589
- this._appendHeaderImage(body, ic);
7590
- const title = document.createElement("div");
7591
- title.style.cssText = `font-size: 16px; font-weight: ${isOverlay ? "700" : (slotStyle == null ? void 0 : slotStyle.weight) ?? "600"}; margin-bottom: ${isOverlay ? "16px" : "12px"}; text-align: ${titleAlign};`;
7592
- title.textContent = ic.nps_question || campaign.title || "How likely are you to recommend us?";
7593
- body.appendChild(title);
7594
- const npsColor = (n) => n <= 6 ? "#ef4444" : n <= 8 ? "#f59e0b" : "#10b981";
7595
- const scale = document.createElement("div");
7596
- scale.style.cssText = isOverlay ? "display: flex; gap: 4px; justify-content: center; flex-wrap: wrap; margin-bottom: 12px;" : "display: grid; grid-template-columns: repeat(11, 1fr); gap: 4px;";
7597
- const onPick = (value) => {
7598
- this.trackEvent(campaign.id, "clicked");
7599
- if (submitUrl) {
7600
- this._submitTokenResponse(submitUrl, { sub_type: "nps_survey", value });
7601
- }
7602
- const followupEnabled = isOverlay && ic.nps_followup_enabled !== false;
7603
- if (followupEnabled) {
7604
- this._renderNpsReasonFollowup(campaign, ic, body, value, text);
7605
- return;
7606
- }
7607
- void this.submitWidgetResponse(campaign, "nps", { score: value });
7608
- this.addAnimationStyles();
7609
- const thanks = document.createElement("div");
7610
- thanks.style.cssText = `font-size: 14px; font-weight: 600; padding: 8px 0; text-align: center; color: ${text};`;
7611
- thanks.textContent = value >= 9 ? "Thanks! We are glad you love it." : value <= 6 ? "Thanks for the honest feedback — we will do better." : "Thank you for your feedback!";
7612
- scale.replaceWith(thanks);
7613
- labels == null ? void 0 : labels.remove();
7614
- if (value >= 9) this._playReaction(body, "affirm", void 0, ic);
7615
- else if (value <= 6) this._playReaction(body, "empathize", void 0, ic);
7616
- else this._playReaction(body, "acknowledge", void 0, ic);
7617
- };
7618
- for (let n = 0; n <= 10; n++) {
7619
- const col = npsColor(n);
7620
- const value = n;
7621
- if (isOverlay) {
7622
- const btn = document.createElement("span");
7623
- btn.style.cssText = `
7624
- width: 28px; height: 28px; border-radius: 6px; display: flex;
7625
- align-items: center; justify-content: center; font-size: 12px;
7626
- font-weight: 700; cursor: pointer; background: ${col}; color: #fff;
7627
- opacity: 0.85; transition: transform 0.1s, opacity 0.15s;
7628
- `;
7629
- btn.textContent = String(n);
7630
- btn.addEventListener("mouseenter", () => {
7631
- btn.style.opacity = "1";
7632
- btn.style.transform = "translateY(-1px)";
7633
- });
7634
- btn.addEventListener("mouseleave", () => {
7635
- btn.style.opacity = "0.85";
7636
- btn.style.transform = "translateY(0)";
7637
- });
7638
- btn.addEventListener("click", () => onPick(value));
7639
- scale.appendChild(btn);
7640
- } else {
7641
- const btn = document.createElement("button");
7642
- btn.style.cssText = `
7643
- padding: 8px 0; border-radius: 6px; border: 1px solid ${col}88;
7644
- background: ${col}14; color: ${col}; font-size: 13px; font-weight: 700;
7645
- cursor: pointer; transition: transform 0.12s, background 0.15s;
7646
- `;
7647
- btn.textContent = String(n);
7648
- btn.addEventListener("mouseenter", () => {
7649
- btn.style.background = col;
7650
- btn.style.color = "#fff";
7651
- btn.style.transform = "translateY(-1px)";
7652
- });
7653
- btn.addEventListener("mouseleave", () => {
7654
- btn.style.background = `${col}14`;
7655
- btn.style.color = col;
7656
- btn.style.transform = "translateY(0)";
7657
- });
7658
- btn.addEventListener("click", () => onPick(value));
7659
- scale.appendChild(btn);
7660
- }
7661
- }
7662
- body.appendChild(scale);
7663
- let labels = null;
7664
- if (isOverlay) {
7665
- labels = document.createElement("div");
7666
- labels.style.cssText = "display: flex; justify-content: space-between; font-size: 11px; opacity: 0.6; margin-bottom: 16px;";
7667
- const notLikely = document.createElement("span");
7668
- notLikely.textContent = "Not likely";
7669
- const veryLikely = document.createElement("span");
7670
- veryLikely.textContent = "Very likely";
7671
- labels.appendChild(notLikely);
7672
- labels.appendChild(veryLikely);
7673
- body.appendChild(labels);
7674
- }
7675
- return body;
7676
- }
7677
- /**
7678
- * NPS reason-capture step (overlay). Wires the per-band `nps_follow_up_*`
7679
- * prompt (promoter / passive / detractor) as the heading and captures an
7680
- * open-text reason (+ optional quick-pick chips). Submits {score, comment}
7681
- * exactly ONCE (deferred from the pick), then shows the thank-you (promoters
7682
- * get an optional review/referral CTA).
7683
- */
7684
- _renderNpsReasonFollowup(campaign, ic, body, value, text) {
7685
- this.addAnimationStyles();
7686
- const promoterThreshold = ic.nps_promoter_threshold || 9;
7687
- const prompt = value >= promoterThreshold ? ic.nps_follow_up_promoter || "What do you love most about us?" : value <= 6 ? ic.nps_follow_up_detractor || "How can we improve?" : ic.nps_follow_up_passive || "What would make us a 10?";
7688
- while (body.firstChild) body.removeChild(body.firstChild);
7689
- const heading = document.createElement("div");
7690
- heading.style.cssText = "font-size: 15px; font-weight: 700; margin-bottom: 10px; text-align: center;";
7691
- heading.textContent = prompt;
7692
- body.appendChild(heading);
7693
- const ta = document.createElement("textarea");
7694
- const chips = Array.isArray(ic.nps_reason_chips) ? ic.nps_reason_chips : [];
7695
- if (chips.length) {
7696
- const chipRow = document.createElement("div");
7697
- chipRow.style.cssText = "display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; margin-bottom: 10px;";
7698
- chips.slice(0, 8).forEach((c) => {
7699
- const chip = document.createElement("button");
7700
- chip.type = "button";
7701
- chip.textContent = c;
7702
- chip.style.cssText = `padding: 5px 11px; border-radius: 999px; border: 1px solid ${text}33; background: transparent; color: ${text || "inherit"}; font-size: 12px; cursor: pointer;`;
7703
- chip.addEventListener("click", () => {
7704
- ta.value = ta.value ? `${ta.value}, ${c}` : c;
7705
- ta.focus();
7706
- });
7707
- chipRow.appendChild(chip);
7708
- });
7709
- body.appendChild(chipRow);
7710
- }
7711
- ta.className = "aegis-rating-comment";
7712
- ta.rows = 3;
7713
- ta.placeholder = "Share a little more…";
7714
- ta.style.cssText = `width: 100%; box-sizing: border-box; padding: 10px; border-radius: 10px; border: 1px solid ${text}33; background: transparent; color: ${text || "inherit"}; font-size: 13px; resize: vertical; margin-bottom: 12px; font-family: inherit;`;
7715
- body.appendChild(ta);
7716
- const st = this.inAppStyle(campaign);
7717
- const row = document.createElement("div");
7718
- row.style.cssText = "display: flex; gap: 8px; justify-content: center;";
7719
- let done = false;
7720
- const finish = (withComment) => {
7721
- if (done) return;
7722
- done = true;
7723
- const comment = withComment ? ta.value.trim() : "";
7724
- void this.submitWidgetResponse(campaign, "nps", comment ? { score: value, comment } : { score: value });
7725
- this._renderNpsThankYou(campaign, ic, body, value >= promoterThreshold);
7726
- };
7727
- const send = document.createElement("button");
7728
- send.type = "button";
7729
- send.textContent = "Send";
7730
- send.style.cssText = `padding: 9px 20px; border-radius: 999px; border: none; background: ${st.btnBg}; color: ${st.btnText}; font-weight: 800; font-size: 13px; cursor: pointer;`;
7731
- send.addEventListener("click", () => finish(true));
7732
- const skip = document.createElement("button");
7733
- skip.type = "button";
7734
- skip.textContent = "Skip";
7735
- skip.style.cssText = `padding: 9px 16px; border-radius: 999px; border: 1px solid ${text}33; background: transparent; color: ${text || "inherit"}; font-weight: 600; font-size: 13px; cursor: pointer;`;
7736
- skip.addEventListener("click", () => finish(false));
7737
- row.appendChild(send);
7738
- row.appendChild(skip);
7739
- body.appendChild(row);
7740
- ta.focus();
7741
- }
7742
- /** NPS thank-you takeover; promoters get an optional review/referral CTA. */
7743
- _renderNpsThankYou(campaign, ic, body, isPromoter) {
7744
- while (body.firstChild) body.removeChild(body.firstChild);
7745
- const modalEl = body.parentElement;
7746
- if (modalEl) modalEl.style.overflow = "visible";
7747
- body.style.position = "relative";
7748
- body.style.minHeight = "176px";
7749
- body.style.display = "flex";
7750
- body.style.flexDirection = "column";
7751
- body.style.alignItems = "center";
7752
- body.style.justifyContent = "center";
7753
- const heading = document.createElement("div");
7754
- heading.style.cssText = "font-size: 16px; font-weight: 700; margin-bottom: 8px; text-align: center;";
7755
- heading.textContent = ic.thank_you_message || (isPromoter ? "Thanks — glad you love it!" : "Thanks for your feedback!");
7756
- body.appendChild(heading);
7757
- const reviewUrl = ic.nps_promoter_url;
7758
- if (isPromoter && reviewUrl) {
7759
- const sub = document.createElement("div");
7760
- sub.style.cssText = "font-size: 13px; opacity: 0.8; margin-bottom: 14px; text-align: center;";
7761
- sub.textContent = "Would you share that with others?";
7762
- body.appendChild(sub);
7763
- const st = this.inAppStyle(campaign);
7764
- const cta = document.createElement("button");
7765
- cta.type = "button";
7766
- cta.textContent = campaign.button_text || "Leave a review";
7767
- cta.style.cssText = `padding: 10px 24px; border-radius: 999px; border: none; background: ${st.btnBg}; color: ${st.btnText}; font-weight: 800; font-size: 13.5px; cursor: pointer;`;
7768
- cta.addEventListener("click", () => {
7769
- this.trackEvent(campaign.id, "clicked", { meta: { action: "nps_review_redirect" } });
7770
- const safe = this.sanitizeUrl(reviewUrl);
7771
- if (safe) this.navigateToCampaignAction(campaign, safe);
7772
- });
7773
- body.appendChild(cta);
7774
- } else {
7775
- this._playReaction(body, isPromoter ? "affirm" : "acknowledge", void 0, ic);
7776
- }
7777
- }
7778
7526
  /**
7779
7527
  * Evaluate the currently armed campaigns against a client-side event
7780
7528
  * and render any that match their `client_trigger`.
@@ -7836,6 +7584,7 @@ const _AegisInAppManager = class _AegisInAppManager {
7836
7584
  this._displaySizeMul = this._sizeMul(
7837
7585
  campaign.interactive_config
7838
7586
  );
7587
+ this._ensureSurveyTemplate(campaign);
7839
7588
  const interactiveSubTypes = /* @__PURE__ */ new Set([
7840
7589
  "spin_wheel",
7841
7590
  "scratch_card",
@@ -7859,11 +7608,9 @@ const _AegisInAppManager = class _AegisInAppManager {
7859
7608
  }
7860
7609
  {
7861
7610
  const ic2 = campaign.interactive_config || {};
7862
- const tpl2 = ic2.card_template;
7863
- const hasTpl2 = !!tpl2 && typeof tpl2 === "object" && Array.isArray(tpl2.elements) && tpl2.elements.length > 0;
7864
7611
  const multiCard2 = campaign.sub_type === "product_recommendation" || campaign.sub_type === "carousel_cards";
7865
7612
  const overlayCardType = campaign.type === "modal" || campaign.type === "banner" || campaign.type === "full_screen" || campaign.type === "half_interstitial";
7866
- if (hasTpl2 && !multiCard2 && overlayCardType) {
7613
+ if (this._hasComposedTemplate(ic2) && !multiCard2 && overlayCardType) {
7867
7614
  this.renderComposedCardOverlay(campaign, ic2);
7868
7615
  this.trackEvent(campaign.id, "impression");
7869
7616
  this.emit("campaign-shown", campaign);
@@ -8010,19 +7757,17 @@ const _AegisInAppManager = class _AegisInAppManager {
8010
7757
  if (campaign.sub_type && renderGame(this.buildRenderContext(campaign), campaign.sub_type)) {
8011
7758
  return;
8012
7759
  }
7760
+ {
7761
+ const isMultiStepFlow = campaign.sub_type === "quiz" || campaign.sub_type === "multi_step_form";
7762
+ if (this._hasComposedTemplate(ic) && !isMultiStepFlow) {
7763
+ this.renderComposedCardOverlay(campaign, ic);
7764
+ return;
7765
+ }
7766
+ }
8013
7767
  switch (campaign.sub_type) {
8014
- case "nps_survey":
8015
- this.renderNPSSurvey(campaign, ic, bg, text);
8016
- break;
8017
7768
  case "countdown_offer":
8018
7769
  this.renderCountdownOffer(campaign, ic, bg, text);
8019
7770
  break;
8020
- case "star_rating":
8021
- this.renderStarRating(campaign, ic, bg, text);
8022
- break;
8023
- case "quick_poll":
8024
- this.renderQuickPoll(campaign, ic, bg, text);
8025
- break;
8026
7771
  case "quiz":
8027
7772
  this.renderQuiz(campaign, ic, bg, text);
8028
7773
  break;
@@ -8189,21 +7934,6 @@ const _AegisInAppManager = class _AegisInAppManager {
8189
7934
  return true;
8190
7935
  }
8191
7936
  // --- Interactive Renderers ---
8192
- renderNPSSurvey(campaign, ic, bg, text) {
8193
- const overlay = this.createOverlay("aegis-in-app-nps-overlay");
8194
- const modal = document.createElement("div");
8195
- modal.style.cssText = `
8196
- max-width: 360px; width: 90%; border-radius: 16px; overflow: hidden;
8197
- background: ${bg}; color: ${text}; animation: aegisScaleIn 0.3s ease;
8198
- box-shadow: 0 16px 48px rgba(0,0,0,0.22);
8199
- `;
8200
- const body = this._buildNPSSurveyBody(campaign, ic, text, "overlay");
8201
- this.addCloseButton(body, overlay, campaign.id);
8202
- modal.appendChild(body);
8203
- overlay.appendChild(modal);
8204
- this.addAnimationStyles();
8205
- document.body.appendChild(overlay);
8206
- }
8207
7937
  renderCountdownOffer(campaign, ic, bg, text) {
8208
7938
  const overlay = this.createOverlay("aegis-in-app-countdown-overlay");
8209
7939
  const modal = document.createElement("div");
@@ -8291,84 +8021,6 @@ const _AegisInAppManager = class _AegisInAppManager {
8291
8021
  this.addAnimationStyles();
8292
8022
  document.body.appendChild(overlay);
8293
8023
  }
8294
- renderStarRating(campaign, ic, bg, text) {
8295
- const overlay = this.createOverlay("aegis-in-app-rating-overlay");
8296
- const modal = document.createElement("div");
8297
- modal.style.cssText = `
8298
- max-width: 320px; width: 90%; border-radius: 16px; overflow: hidden;
8299
- background: ${bg}; color: ${text}; animation: aegisScaleIn 0.3s ease;
8300
- box-shadow: 0 16px 48px rgba(0,0,0,0.22);
8301
- `;
8302
- const body = this._buildStarRatingBody(campaign, ic, text, "overlay");
8303
- this.addCloseButton(body, overlay, campaign.id);
8304
- modal.appendChild(body);
8305
- overlay.appendChild(modal);
8306
- this.addAnimationStyles();
8307
- document.body.appendChild(overlay);
8308
- }
8309
- renderQuickPoll(campaign, ic, bg, text) {
8310
- const overlay = this.createOverlay("aegis-in-app-poll-overlay");
8311
- const modal = document.createElement("div");
8312
- modal.style.cssText = `
8313
- max-width: 320px; width: 90%; border-radius: 16px; overflow: hidden;
8314
- background: ${bg}; color: ${text}; animation: aegisScaleIn 0.3s ease;
8315
- box-shadow: 0 16px 48px rgba(0,0,0,0.22);
8316
- `;
8317
- const body = document.createElement("div");
8318
- body.style.cssText = "padding: 24px; text-align: center;";
8319
- const title = document.createElement("div");
8320
- title.style.cssText = `font-size: 16px; font-weight: 700; margin-bottom: ${campaign.body ? "6px" : "16px"};`;
8321
- title.textContent = campaign.title || "Quick question";
8322
- body.appendChild(title);
8323
- if (campaign.body) {
8324
- const sub = document.createElement("div");
8325
- sub.style.cssText = `font-size: 13px; opacity: 0.8; margin-bottom: 16px; color: ${text};`;
8326
- sub.textContent = campaign.body;
8327
- body.appendChild(sub);
8328
- }
8329
- const options = ic.poll_options || [];
8330
- const optionsList = document.createElement("div");
8331
- optionsList.style.cssText = "display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px;";
8332
- const buttons = [];
8333
- options.forEach((opt, i) => {
8334
- const optBtn = document.createElement("button");
8335
- optBtn.style.cssText = `
8336
- position: relative; overflow: hidden;
8337
- padding: 10px 16px; border-radius: 10px; border: 1px solid ${text}33;
8338
- background: transparent; color: ${text}; font-size: 14px; cursor: pointer;
8339
- text-align: left; transition: border-color 0.2s ease;
8340
- `;
8341
- const lbl = document.createElement("span");
8342
- lbl.style.cssText = "position: relative; z-index: 1; display: flex; justify-content: space-between; gap: 8px;";
8343
- const lblText = document.createElement("span");
8344
- lblText.textContent = opt;
8345
- const lblPct = document.createElement("span");
8346
- lblPct.style.cssText = "font-weight: 700; opacity: 0; transition: opacity 0.3s ease;";
8347
- lbl.appendChild(lblText);
8348
- lbl.appendChild(lblPct);
8349
- optBtn.appendChild(lbl);
8350
- optBtn.addEventListener("click", () => {
8351
- this.trackEvent(campaign.id, "clicked");
8352
- buttons.forEach((b) => b.disabled = true);
8353
- this._paintPollResult(buttons, options, i, null, text);
8354
- void this.submitWidgetResponse(campaign, "poll", { option_index: i, option_label: opt }).then((resp) => {
8355
- const dist = this._readPollDistribution(resp, options.length);
8356
- if (dist) this._paintPollResult(buttons, options, i, dist, text);
8357
- });
8358
- const modalEl = body.parentElement;
8359
- if (modalEl) modalEl.style.overflow = "visible";
8360
- this._playReaction(body, "affirm", "check.json", ic);
8361
- });
8362
- buttons.push(optBtn);
8363
- optionsList.appendChild(optBtn);
8364
- });
8365
- body.appendChild(optionsList);
8366
- this.addCloseButton(body, overlay, campaign.id);
8367
- modal.appendChild(body);
8368
- overlay.appendChild(modal);
8369
- this.addAnimationStyles();
8370
- document.body.appendChild(overlay);
8371
- }
8372
8024
  /**
8373
8025
  * Multi-step form (SDK Demo v4 W1b) — in-app render type matching the
8374
8026
  * Plotline multi-step flow pattern: a progress bar + per-step question with
@@ -8400,6 +8052,13 @@ const _AegisInAppManager = class _AegisInAppManager {
8400
8052
  const total = steps.length;
8401
8053
  let current = 0;
8402
8054
  const answers = new Array(total).fill(null);
8055
+ const flow = createFlowAccumulator();
8056
+ const viewedSteps = /* @__PURE__ */ new Set();
8057
+ const fireStepViewed = (idx) => {
8058
+ if (viewedSteps.has(idx)) return;
8059
+ viewedSteps.add(idx);
8060
+ void this.trackEvent(campaign.id, "impression", { stepId: `step_${idx + 1}` });
8061
+ };
8403
8062
  const progressTrack = document.createElement("div");
8404
8063
  progressTrack.style.cssText = `height: 4px; border-radius: 2px; background: ${text}22; margin-bottom: 16px; overflow: hidden;`;
8405
8064
  const progressFill = document.createElement("div");
@@ -8429,6 +8088,7 @@ const _AegisInAppManager = class _AegisInAppManager {
8429
8088
  const step = steps[current] || {};
8430
8089
  const q = step.question || step.label || `Step ${current + 1}`;
8431
8090
  const opts = Array.isArray(step.options) ? step.options : [];
8091
+ fireStepViewed(current);
8432
8092
  progressFill.style.width = `${Math.round(current / total * 100)}%`;
8433
8093
  stepLabel.textContent = `Step ${current + 1} of ${total}`;
8434
8094
  question.textContent = q;
@@ -8440,6 +8100,7 @@ const _AegisInAppManager = class _AegisInAppManager {
8440
8100
  ob.style.cssText = `padding: 10px 16px; border-radius: 10px; border: 1px solid ${text}33; background: ${selected ? text + "22" : "transparent"}; color: ${text}; font-size: 14px; cursor: pointer; text-align: left;`;
8441
8101
  ob.addEventListener("click", () => {
8442
8102
  answers[current] = opt;
8103
+ flow.recordStep({ step_id: `step_${current + 1}`, response_type: "form_step", payload: { question: q, answer: opt } });
8443
8104
  renderStep();
8444
8105
  });
8445
8106
  optionsList.appendChild(ob);
@@ -8463,8 +8124,10 @@ const _AegisInAppManager = class _AegisInAppManager {
8463
8124
  renderStep();
8464
8125
  } else {
8465
8126
  progressFill.style.width = "100%";
8127
+ flow.markCompleted();
8128
+ void this.trackEvent(campaign.id, "impression", { stepId: "flow_completed" });
8466
8129
  void this.trackEvent(campaign.id, "clicked", { meta: { answers } });
8467
- void this.submitWidgetResponse(campaign, "form", { answers });
8130
+ void this.submitWidgetResponse(campaign, "form", { answers, flow: flow.consolidated() });
8468
8131
  this._playReaction(body, "affirm", "check.json", ic);
8469
8132
  window.setTimeout(() => {
8470
8133
  if (overlay.parentNode) document.body.removeChild(overlay);