@magicfeedback/popup-sdk 1.0.10 → 1.0.11

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.mjs CHANGED
@@ -1774,7 +1774,7 @@ function ensureResponsiveStyles(popup) {
1774
1774
  `;
1775
1775
  popup.appendChild(style);
1776
1776
  }
1777
- async function renderPopup(container, surveyId, productId, actions, emit, onClose) {
1777
+ async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production") {
1778
1778
  let surveyCompletedEmitted = false;
1779
1779
  let stylesInjected = false;
1780
1780
  const popup = document.createElement("div");
@@ -2058,7 +2058,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
2058
2058
  setLoading(false);
2059
2059
  return;
2060
2060
  }
2061
- import_native.default.init({ debug: true, env: "prod" });
2061
+ import_native.default.init({ debug: true, env: env === "production" ? "prod" : "dev" });
2062
2062
  formInstance = import_native.default.form(surveyId, productId);
2063
2063
  const generateOptions = {
2064
2064
  addButton: false,
@@ -2209,6 +2209,9 @@ function setupTrigger(popups, trigger) {
2209
2209
  case "exit":
2210
2210
  setupExitTrigger(popups, trigger);
2211
2211
  break;
2212
+ case "click":
2213
+ setupClickTrigger(popups, trigger);
2214
+ break;
2212
2215
  }
2213
2216
  }
2214
2217
  function setupTimeTrigger(popups, trigger) {
@@ -2231,23 +2234,120 @@ function setupScrollTrigger(popups, trigger) {
2231
2234
  popups.debug(`Scroll trigger set for ${threshold}%`);
2232
2235
  }
2233
2236
  function setupExitTrigger(popups, trigger) {
2234
- const handler = (e) => {
2235
- if (e.clientY <= 0) {
2236
- popups.triggerSurvey(trigger.surveyId);
2237
- document.removeEventListener("mouseout", handler);
2237
+ let fired = false;
2238
+ let lastUrl = typeof window !== "undefined" ? window.location.href : "";
2239
+ const historyRef = typeof window !== "undefined" ? window.history : null;
2240
+ const originalPushState = historyRef ? historyRef.pushState.bind(historyRef) : null;
2241
+ const originalReplaceState = historyRef ? historyRef.replaceState.bind(historyRef) : null;
2242
+ const cleanup = () => {
2243
+ document.removeEventListener("click", handleNavClick, true);
2244
+ window.removeEventListener("popstate", handlePopState);
2245
+ if (historyRef && originalPushState) historyRef.pushState = originalPushState;
2246
+ if (historyRef && originalReplaceState) historyRef.replaceState = originalReplaceState;
2247
+ };
2248
+ const fireOnce = () => {
2249
+ if (fired) return;
2250
+ fired = true;
2251
+ popups.triggerSurvey(trigger.surveyId);
2252
+ cleanup();
2253
+ };
2254
+ const handleNavClick = (e) => {
2255
+ if (fired) return;
2256
+ if (e.defaultPrevented) return;
2257
+ if (e.button !== 0) return;
2258
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
2259
+ const target = e.target?.closest?.("a");
2260
+ if (!target) return;
2261
+ const href = target.getAttribute("href");
2262
+ if (!href || href.startsWith("#")) return;
2263
+ const targetAttr = target.getAttribute("target");
2264
+ if (targetAttr && targetAttr !== "_self") return;
2265
+ let url;
2266
+ try {
2267
+ url = new URL(href, window.location.href);
2268
+ } catch {
2269
+ return;
2270
+ }
2271
+ if (url.origin !== window.location.origin) return;
2272
+ e.preventDefault();
2273
+ fireOnce();
2274
+ };
2275
+ const shouldBlockUrl = (rawUrl) => {
2276
+ if (!rawUrl) return false;
2277
+ let url;
2278
+ try {
2279
+ url = rawUrl instanceof URL ? rawUrl : new URL(rawUrl, window.location.href);
2280
+ } catch {
2281
+ return false;
2238
2282
  }
2283
+ if (url.origin !== window.location.origin) return false;
2284
+ if (url.href === window.location.href) return false;
2285
+ return true;
2239
2286
  };
2240
- document.addEventListener("mouseout", handler);
2241
- popups.debug("Exit intent trigger set");
2287
+ const handlePopState = () => {
2288
+ if (fired) return;
2289
+ fireOnce();
2290
+ if (historyRef && originalPushState && lastUrl) {
2291
+ originalPushState(null, document.title, lastUrl);
2292
+ }
2293
+ };
2294
+ if (historyRef && originalPushState && originalReplaceState) {
2295
+ historyRef.pushState = function(data, unused, url) {
2296
+ if (!fired && shouldBlockUrl(url)) {
2297
+ fireOnce();
2298
+ return;
2299
+ }
2300
+ const result = originalPushState(data, unused, url);
2301
+ lastUrl = window.location.href;
2302
+ return result;
2303
+ };
2304
+ historyRef.replaceState = function(data, unused, url) {
2305
+ if (!fired && shouldBlockUrl(url)) {
2306
+ fireOnce();
2307
+ return;
2308
+ }
2309
+ const result = originalReplaceState(data, unused, url);
2310
+ lastUrl = window.location.href;
2311
+ return result;
2312
+ };
2313
+ }
2314
+ document.addEventListener("click", handleNavClick, true);
2315
+ window.addEventListener("popstate", handlePopState);
2316
+ popups.debug("Exit intent trigger set (navigation/spa)");
2317
+ }
2318
+ function setupClickTrigger(popups, trigger) {
2319
+ const targetId = typeof trigger.value === "string" ? trigger.value : "";
2320
+ if (!targetId) {
2321
+ popups.debug("Click trigger missing element id", trigger);
2322
+ return;
2323
+ }
2324
+ const attach = () => {
2325
+ const el = document.getElementById(targetId);
2326
+ if (!el) {
2327
+ popups.debug("Click trigger target not found", targetId);
2328
+ return false;
2329
+ }
2330
+ const handler = (e) => {
2331
+ if (e.defaultPrevented) return;
2332
+ popups.triggerSurvey(trigger.surveyId);
2333
+ el.removeEventListener("click", handler);
2334
+ };
2335
+ el.addEventListener("click", handler);
2336
+ popups.debug(`Click trigger set for #${targetId}`);
2337
+ return true;
2338
+ };
2339
+ if (typeof document === "undefined") return;
2340
+ if (attach()) return;
2341
+ window.addEventListener("DOMContentLoaded", () => attach(), { once: true });
2242
2342
  }
2243
2343
 
2244
2344
  // src/config/env.ts
2245
2345
  var ENVIRONMENT_MAP = {
2246
2346
  development: {
2247
- apiBaseUrl: "https://api-dev.magicfeedback.com"
2347
+ apiBaseUrl: "https://api-dev.deepdots.com"
2248
2348
  },
2249
2349
  production: {
2250
- apiBaseUrl: "https://api.magicfeedback.com"
2350
+ apiBaseUrl: "https://api.deepdots.com"
2251
2351
  }
2252
2352
  };
2253
2353
  function resolveEnvironment(nodeEnv) {
@@ -2329,10 +2429,10 @@ var BrowserPopupRenderer = class {
2329
2429
  document.body.appendChild(this.container);
2330
2430
  }
2331
2431
  }
2332
- show(surveyId, productId, actions, emit, onClose) {
2432
+ show(surveyId, productId, actions, emit, onClose, env = "production") {
2333
2433
  if (!this.container) this.init();
2334
2434
  if (!this.container) return;
2335
- renderPopup(this.container, surveyId, productId, actions, emit, onClose);
2435
+ renderPopup(this.container, surveyId, productId, actions, emit, onClose, env);
2336
2436
  }
2337
2437
  hide() {
2338
2438
  if (this.container) {
@@ -2365,8 +2465,12 @@ var DeepdotsPopups = class {
2365
2465
  this.pendingAutoLaunch = false;
2366
2466
  this.answeredSurveys = /* @__PURE__ */ new Set();
2367
2467
  this.lastShown = /* @__PURE__ */ new Map();
2468
+ // popupId -> timestamp mostrado
2469
+ this.surveyToPopupId = /* @__PURE__ */ new Map();
2470
+ // surveyId -> popupId
2471
+ this.baseUrl = "";
2472
+ this.env = "production";
2368
2473
  }
2369
- // popupId -> timestamp mostrado
2370
2474
  /** Initialize the SDK with configuration */
2371
2475
  init(config) {
2372
2476
  if (this.initialized) {
@@ -2374,12 +2478,14 @@ var DeepdotsPopups = class {
2374
2478
  return;
2375
2479
  }
2376
2480
  const env = resolveEnvironment(config.nodeEnv);
2481
+ this.baseUrl = env.apiBaseUrl;
2482
+ this.env = config.nodeEnv || "production";
2377
2483
  this.config = {
2378
2484
  apiKey: config.apiKey || void 0,
2379
- baseUrl: env.apiBaseUrl,
2380
2485
  mode: config.mode || "client",
2381
2486
  debug: config.debug || false,
2382
- popups: config.popups || []
2487
+ popups: config.popups || [],
2488
+ userId: config.userId || void 0
2383
2489
  };
2384
2490
  this.initialized = true;
2385
2491
  this.log("SDK initialized", this.config);
@@ -2428,8 +2534,9 @@ var DeepdotsPopups = class {
2428
2534
  if (isDefinition) {
2429
2535
  const def = options;
2430
2536
  this.log("Showing popup (definition)", def);
2537
+ this.surveyToPopupId.set(def.surveyId, def.id);
2431
2538
  this.renderPopup(def.surveyId, def.productId, def.actions);
2432
- this.emitEvent("popup_shown", def.surveyId);
2539
+ this.emitEvent("popup_shown", def.surveyId, { popupId: def.id });
2433
2540
  } else {
2434
2541
  const opt = options;
2435
2542
  this.log("Showing popup (legacy options)", opt);
@@ -2458,9 +2565,10 @@ var DeepdotsPopups = class {
2458
2565
  configureTriggersFromDefinitions() {
2459
2566
  const derived = [];
2460
2567
  const validDefs = this.validatePopupDefinitions(this.popupDefinitions);
2568
+ console.log("validDefs", validDefs);
2461
2569
  validDefs.forEach((def) => {
2462
- if (!def.trigger) return;
2463
- const t = def.trigger;
2570
+ if (!def.triggers) return;
2571
+ const t = def.triggers;
2464
2572
  let type;
2465
2573
  switch (t.type) {
2466
2574
  case "time_on_page":
@@ -2472,11 +2580,14 @@ var DeepdotsPopups = class {
2472
2580
  case "exit":
2473
2581
  type = "exit";
2474
2582
  break;
2583
+ case "click":
2584
+ type = "click";
2585
+ break;
2475
2586
  default:
2476
2587
  this.debug("Unsupported trigger type", t.type);
2477
2588
  return;
2478
2589
  }
2479
- const value = type === "time" ? t.value * 1e3 : t.value;
2590
+ const value = type === "time" && typeof t.value === "number" ? t.value * 1e3 : t.value;
2480
2591
  derived.push({ type, value, surveyId: def.surveyId });
2481
2592
  });
2482
2593
  if (derived.length) {
@@ -2498,10 +2609,52 @@ var DeepdotsPopups = class {
2498
2609
  this.lastShown.set(def.id, Date.now());
2499
2610
  }
2500
2611
  shouldShow(def) {
2501
- const conditions = def.trigger.condition || [];
2612
+ if (!this.matchesSegmentsPath(def)) {
2613
+ return false;
2614
+ }
2615
+ const conditions = def.triggers.condition || [];
2502
2616
  if (!conditions.length) return true;
2503
2617
  return conditions.every((c) => this.evaluateCondition(def, c));
2504
2618
  }
2619
+ matchesSegmentsPath(def) {
2620
+ const paths = def.segments?.path;
2621
+ if (!paths || paths.length === 0) return true;
2622
+ if (typeof window === "undefined" || !window.location) {
2623
+ this.debug("No window.location available for path comparison", { popupId: def.id, paths });
2624
+ return true;
2625
+ }
2626
+ const currentPath = window.location.pathname || "";
2627
+ const currentHref = window.location.href || "";
2628
+ const normalize = (value) => {
2629
+ if (value.length > 1 && value.endsWith("/")) return value.slice(0, -1);
2630
+ return value;
2631
+ };
2632
+ const normalizedPath = normalize(currentPath);
2633
+ const normalizedHref = normalize(currentHref);
2634
+ const matches = paths.some((rawCandidate) => {
2635
+ const candidate = normalize(rawCandidate);
2636
+ let match = false;
2637
+ if (candidate.startsWith("http://") || candidate.startsWith("https://")) {
2638
+ match = normalizedHref === candidate;
2639
+ } else if (candidate.startsWith("/")) {
2640
+ match = normalizedHref.includes(candidate);
2641
+ } else {
2642
+ match = normalizedPath === candidate;
2643
+ }
2644
+ this.debug("Path comparison", {
2645
+ popupId: def.id,
2646
+ candidate,
2647
+ currentPath: normalizedPath,
2648
+ currentHref: normalizedHref,
2649
+ match
2650
+ });
2651
+ return match;
2652
+ });
2653
+ if (!matches) {
2654
+ this.debug("No path match for popup", { popupId: def.id, paths, currentPath: normalizedPath });
2655
+ }
2656
+ return matches;
2657
+ }
2505
2658
  evaluateCondition(def, condition) {
2506
2659
  if (!condition.answered && this.answeredSurveys.has(def.surveyId)) {
2507
2660
  return false;
@@ -2520,29 +2673,59 @@ var DeepdotsPopups = class {
2520
2673
  markSurveyAnswered(surveyId) {
2521
2674
  this.answeredSurveys.add(surveyId);
2522
2675
  }
2523
- /** Simula fetch al servidor para obtener popups */
2676
+ /** Fetch al servidor para obtener popups */
2524
2677
  async fetchPopupsFromServer() {
2525
- await new Promise((res) => setTimeout(res, 50));
2526
- return this.generateFakePopups();
2678
+ const apiKey = this.config?.apiKey;
2679
+ const baseUrl = this.baseUrl;
2680
+ if (!apiKey || !baseUrl) {
2681
+ this.log("Missing apiKey or baseUrl. Skipping popups fetch.");
2682
+ return [];
2683
+ }
2684
+ const endpoint = `${baseUrl}/sdk/${encodeURIComponent(apiKey)}/popups`;
2685
+ try {
2686
+ const response = await fetch(endpoint);
2687
+ if (!response.ok) {
2688
+ this.log("Failed to fetch popups", response.status, response.statusText);
2689
+ return [];
2690
+ }
2691
+ const raw = await response.text();
2692
+ const parsed = JSON.parse(raw);
2693
+ if (!Array.isArray(parsed)) {
2694
+ this.log("Unexpected popups payload", parsed);
2695
+ return [];
2696
+ }
2697
+ console.log("res: ", parsed, this.validatePopupDefinitions(parsed));
2698
+ return parsed;
2699
+ } catch (error) {
2700
+ this.log("Error fetching popups", error);
2701
+ return [];
2702
+ }
2527
2703
  }
2528
- /** Genera datos fake para pruebas en modo server */
2529
- generateFakePopups() {
2530
- return [
2531
- {
2532
- id: "popup-123",
2533
- title: "Would you like to help us improve?",
2534
- message: "<p>Take a quick survey and share your thoughts.</p>",
2535
- trigger: { type: "time_on_page", value: 1, condition: [{ answered: false, cooldownDays: 7 }] },
2536
- actions: {
2537
- accept: { label: "Take Survey", surveyId: "survey-abc" },
2538
- decline: { label: "Not Now", cooldownDays: 7 }
2539
- },
2540
- surveyId: "survey-abc",
2541
- productId: "product-xyz",
2542
- style: { theme: "light", position: "bottom-right", imageUrl: null },
2543
- segments: { lang: ["en"], path: ["/checkout"] }
2704
+ /** Notifica eventos de popup a la API */
2705
+ async postPopupEvent(status, popupId, userId) {
2706
+ const apiKey = this.config?.apiKey;
2707
+ const baseUrl = this.baseUrl;
2708
+ if (!apiKey || !baseUrl) {
2709
+ return;
2710
+ }
2711
+ const endpoint = `${baseUrl}/sdk/popups`;
2712
+ try {
2713
+ const response = await fetch(endpoint, {
2714
+ method: "POST",
2715
+ headers: { "Content-Type": "application/json" },
2716
+ body: JSON.stringify({
2717
+ publicKey: apiKey,
2718
+ status,
2719
+ popupId,
2720
+ userId: userId || void 0
2721
+ })
2722
+ });
2723
+ if (!response.ok) {
2724
+ this.log("Failed to post popup event", response.status, response.statusText);
2544
2725
  }
2545
- ];
2726
+ } catch (error) {
2727
+ this.log("Error posting popup event", error);
2728
+ }
2546
2729
  }
2547
2730
  /** Add an event listener */
2548
2731
  on(eventType, listener) {
@@ -2582,11 +2765,26 @@ var DeepdotsPopups = class {
2582
2765
  /** Render the popup UI */
2583
2766
  renderPopup(surveyId, productId, actions) {
2584
2767
  if (this.renderer) {
2585
- this.renderer.show(surveyId, productId, actions, (type, id, payload) => this.emitEvent(type, id, payload), () => this.hidePopup());
2768
+ this.renderer.show(
2769
+ surveyId,
2770
+ productId,
2771
+ actions,
2772
+ (type, id, payload) => this.emitEvent(type, id, payload),
2773
+ () => this.hidePopup(),
2774
+ this.env
2775
+ );
2586
2776
  return;
2587
2777
  }
2588
2778
  if (!this.popupContainer) return;
2589
- renderPopup(this.popupContainer, surveyId, productId, actions, (type, id, payload) => this.emitEvent(type, id, payload), () => this.hidePopup());
2779
+ renderPopup(
2780
+ this.popupContainer,
2781
+ surveyId,
2782
+ productId,
2783
+ actions,
2784
+ (type, id, payload) => this.emitEvent(type, id, payload),
2785
+ () => this.hidePopup(),
2786
+ this.env
2787
+ );
2590
2788
  }
2591
2789
  /** Hide the popup */
2592
2790
  hidePopup() {
@@ -2606,6 +2804,16 @@ var DeepdotsPopups = class {
2606
2804
  if (type === "survey_completed") {
2607
2805
  this.markSurveyAnswered(surveyId);
2608
2806
  }
2807
+ if (type === "popup_shown" || type === "survey_completed") {
2808
+ const popupIdFromData = data?.popupId;
2809
+ const popupId = popupIdFromData || this.surveyToPopupId.get(surveyId);
2810
+ if (popupId) {
2811
+ const userIdFromData = data?.userId;
2812
+ void this.postPopupEvent(type === "popup_shown" ? "opened" : "completed", popupId, userIdFromData || this.config?.userId);
2813
+ } else {
2814
+ this.debug("No popupId available to post event", { type, surveyId });
2815
+ }
2816
+ }
2609
2817
  const listeners = this.listeners.get(type);
2610
2818
  if (listeners) {
2611
2819
  listeners.forEach((listener) => {
@@ -2638,10 +2846,26 @@ var DeepdotsPopups = class {
2638
2846
  isPopupDefinition(value) {
2639
2847
  if (typeof value !== "object" || value === null) return false;
2640
2848
  const v = value;
2641
- return typeof v.id === "string" && typeof v.title === "string" && typeof v.message === "string" && typeof v.surveyId === "string" && typeof v.productId === "string" && !!v.trigger;
2849
+ const trigger = v.triggers || v.triggers;
2850
+ if (!trigger) return false;
2851
+ return typeof v.id === "string" && typeof v.title === "string" && typeof v.message === "string" && typeof v.surveyId === "string" && typeof v.productId === "string";
2852
+ }
2853
+ normalizePopupDefinition(def) {
2854
+ if (typeof def !== "object" || def === null) return null;
2855
+ const raw = def;
2856
+ const trigger = raw.triggers || raw.triggers;
2857
+ if (!trigger) return null;
2858
+ const condition = trigger.condition ?? raw.conditions ?? [];
2859
+ return {
2860
+ ...raw,
2861
+ triggers: {
2862
+ ...trigger,
2863
+ condition
2864
+ }
2865
+ };
2642
2866
  }
2643
2867
  validatePopupDefinitions(defs) {
2644
- return defs.filter((d) => this.isPopupDefinition(d));
2868
+ return defs.map((def) => this.normalizePopupDefinition(def)).filter((def) => !!def && this.isPopupDefinition(def));
2645
2869
  }
2646
2870
  };
2647
2871
  export {