@zerocost/sdk 0.13.0 → 0.15.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.
@@ -1,2 +1,3 @@
1
1
  export declare const EDGE_FUNCTION_BASE = "https://mwbgzpbuoojqsuxduieo.supabase.co/functions/v1";
2
+ export { ZEROCOST_BASE_URL } from './constants';
2
3
  export declare function getBaseUrl(custom?: string): string;
@@ -1,12 +1,3 @@
1
- /**
2
- * consent-ui.ts — Pure DOM consent popup for the Zerocost SDK.
3
- *
4
- * - Desktop: centered card modal (max 480px)
5
- * - Mobile (≤640px): bottom-sheet modal
6
- * - Themes: light / dark / auto
7
- * - Non-dismissable (no Escape, no backdrop click)
8
- * - Returns a Promise that resolves with the user's toggle selections
9
- */
10
1
  export interface ConsentUIOptions {
11
2
  appName: string;
12
3
  theme: 'light' | 'dark' | 'auto';
@@ -34,4 +34,6 @@ export declare class ConsentManager {
34
34
  private getOrCreateUserId;
35
35
  private generateUUID;
36
36
  private submitToServer;
37
+ private injectSettingsButton;
38
+ private removeSettingsButton;
37
39
  }
@@ -0,0 +1,6 @@
1
+ export declare const ZEROCOST_DOMAINS: {
2
+ MAIN: string;
3
+ APP: string;
4
+ DOCS: string;
5
+ };
6
+ export declare const ZEROCOST_BASE_URL: string;
package/dist/index.cjs CHANGED
@@ -28,6 +28,14 @@ __export(index_exports, {
28
28
  });
29
29
  module.exports = __toCommonJS(index_exports);
30
30
 
31
+ // src/core/constants.ts
32
+ var ZEROCOST_DOMAINS = {
33
+ MAIN: "https://zerocost.lovable.app",
34
+ APP: "https://app.zerocost.com",
35
+ DOCS: "https://docs.zerocost.com"
36
+ };
37
+ var ZEROCOST_BASE_URL = ZEROCOST_DOMAINS.MAIN;
38
+
31
39
  // src/core/config.ts
32
40
  var EDGE_FUNCTION_BASE = "https://mwbgzpbuoojqsuxduieo.supabase.co/functions/v1";
33
41
  function getBaseUrl(custom) {
@@ -70,7 +78,7 @@ var ZerocostClient = class {
70
78
  ...body || {},
71
79
  app_id: this.config.appId
72
80
  };
73
- this.log(`\u2192 ${url}`, payload);
81
+ this.log(`\u2192 ${path}`, payload);
74
82
  const res = await fetch(url, {
75
83
  method: "POST",
76
84
  headers: {
@@ -89,7 +97,8 @@ var ZerocostClient = class {
89
97
  }
90
98
  log(message, data) {
91
99
  if (this.config.debug) {
92
- console.log(`[Zerocost] ${message}`, data ?? "");
100
+ const sanitizedMessage = typeof message === "string" ? message.replace(/https:\/\/[a-z0-9.-]+\.supabase\.co/gi, "[INFRA]") : message;
101
+ console.log(`[Zerocost] ${sanitizedMessage}`, data ?? "");
93
102
  }
94
103
  }
95
104
  };
@@ -1354,13 +1363,14 @@ function injectStyles(theme) {
1354
1363
  `;
1355
1364
  }
1356
1365
  const css = `
1366
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
1357
1367
  ${themeRule}
1358
1368
 
1359
1369
  .zc-consent-root * {
1360
1370
  box-sizing: border-box;
1361
1371
  margin: 0;
1362
1372
  padding: 0;
1363
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, sans-serif;
1373
+ font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto, sans-serif;
1364
1374
  }
1365
1375
 
1366
1376
  .zc-consent-backdrop {
@@ -1389,10 +1399,10 @@ function injectStyles(theme) {
1389
1399
  border: 1px solid var(--zc-border);
1390
1400
  border-radius: 16px;
1391
1401
  width: 100%;
1392
- max-width: 460px;
1402
+ max-width: 440px;
1393
1403
  max-height: 90vh;
1394
1404
  overflow-y: auto;
1395
- padding: 28px 24px 24px;
1405
+ padding: 24px 20px 20px;
1396
1406
  animation: zc-fade-in 200ms ease;
1397
1407
  }
1398
1408
 
@@ -1416,7 +1426,7 @@ function injectStyles(theme) {
1416
1426
  display: flex;
1417
1427
  align-items: center;
1418
1428
  gap: 10px;
1419
- margin-bottom: 6px;
1429
+ margin-bottom: 4px;
1420
1430
  }
1421
1431
 
1422
1432
  .zc-consent-logo {
@@ -1446,21 +1456,21 @@ function injectStyles(theme) {
1446
1456
  font-size: 13px;
1447
1457
  color: var(--zc-text-secondary);
1448
1458
  line-height: 1.5;
1449
- margin-bottom: 20px;
1459
+ margin-bottom: 16px;
1450
1460
  }
1451
1461
 
1452
1462
  .zc-consent-toggles {
1453
1463
  display: flex;
1454
1464
  flex-direction: column;
1455
1465
  gap: 10px;
1456
- margin-bottom: 20px;
1466
+ margin-bottom: 16px;
1457
1467
  }
1458
1468
 
1459
1469
  .zc-consent-toggle-card {
1460
1470
  background: var(--zc-surface);
1461
1471
  border: 1px solid var(--zc-border);
1462
1472
  border-radius: 12px;
1463
- padding: 14px 16px;
1473
+ padding: 12px 14px;
1464
1474
  }
1465
1475
 
1466
1476
  .zc-consent-toggle-row {
@@ -1545,7 +1555,7 @@ function injectStyles(theme) {
1545
1555
  flex-wrap: wrap;
1546
1556
  gap: 4px 12px;
1547
1557
  justify-content: center;
1548
- margin-bottom: 16px;
1558
+ margin-bottom: 14px;
1549
1559
  }
1550
1560
 
1551
1561
  .zc-consent-footer a {
@@ -1588,6 +1598,44 @@ function injectStyles(theme) {
1588
1598
  .zc-consent-confirm:active {
1589
1599
  opacity: 0.75;
1590
1600
  }
1601
+
1602
+ /* Floating settings button */
1603
+ .zc-settings-btn {
1604
+ position: fixed;
1605
+ width: 44px;
1606
+ height: 44px;
1607
+ border-radius: 50%;
1608
+ background: var(--zc-bg);
1609
+ border: 1px solid var(--zc-border);
1610
+ color: var(--zc-text);
1611
+ display: flex;
1612
+ align-items: center;
1613
+ justify-content: center;
1614
+ cursor: pointer;
1615
+ z-index: 999998;
1616
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
1617
+ transition: transform 200ms, border-color 200ms;
1618
+ }
1619
+
1620
+ .zc-settings-btn:hover {
1621
+ transform: scale(1.05);
1622
+ border-color: var(--zc-text-secondary);
1623
+ }
1624
+
1625
+ .zc-settings-btn svg {
1626
+ width: 20px;
1627
+ height: 20px;
1628
+ }
1629
+
1630
+ .zc-settings-bottom-left { bottom: 20px; left: 20px; }
1631
+ .zc-settings-bottom-right { bottom: 20px; right: 20px; }
1632
+ .zc-settings-top-left { top: 20px; left: 20px; }
1633
+ .zc-settings-top-right { top: 20px; right: 20px; }
1634
+
1635
+ @media (max-width: 640px) {
1636
+ .zc-settings-btn { width: 40px; height: 40px; }
1637
+ .zc-settings-bottom-left, .zc-settings-bottom-right { bottom: 16px; }
1638
+ }
1591
1639
  `;
1592
1640
  const style = document.createElement("style");
1593
1641
  style.id = STYLE_ID;
@@ -1670,33 +1718,33 @@ function showConsentUI(options) {
1670
1718
  card.appendChild(subtitle);
1671
1719
  const toggles = document.createElement("div");
1672
1720
  toggles.className = "zc-consent-toggles";
1673
- const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
1721
+ const zerocostBaseUrl = ZEROCOST_BASE_URL;
1674
1722
  toggles.appendChild(createToggleCard(
1675
1723
  "zc-toggle-ads",
1676
1724
  "Ads",
1677
1725
  "Contextual, non-intrusive ads. No cookies or browsing history used.",
1678
- `${baseUrl}/consent/ads`,
1726
+ `${zerocostBaseUrl}/consent/ads`,
1679
1727
  defaults.ads
1680
1728
  ));
1681
1729
  toggles.appendChild(createToggleCard(
1682
1730
  "zc-toggle-usage",
1683
1731
  "Usage data",
1684
1732
  "Anonymized usage patterns. No personal information is shared.",
1685
- `${baseUrl}/consent/usage-data`,
1733
+ `${zerocostBaseUrl}/consent/usage-data`,
1686
1734
  defaults.usageData
1687
1735
  ));
1688
1736
  toggles.appendChild(createToggleCard(
1689
1737
  "zc-toggle-ai",
1690
1738
  "AI interactions",
1691
1739
  "Anonymized conversation data used for AI research.",
1692
- `${baseUrl}/consent/ai-interactions`,
1740
+ `${zerocostBaseUrl}/consent/ai-interactions`,
1693
1741
  defaults.aiInteractions
1694
1742
  ));
1695
1743
  card.appendChild(toggles);
1696
1744
  const footer = document.createElement("div");
1697
1745
  footer.className = "zc-consent-footer";
1698
1746
  const ppLink = document.createElement("a");
1699
- ppLink.href = privacyPolicyUrl || `${baseUrl}/privacy`;
1747
+ ppLink.href = privacyPolicyUrl || `${zerocostBaseUrl}/privacy`;
1700
1748
  ppLink.target = "_blank";
1701
1749
  ppLink.rel = "noopener noreferrer";
1702
1750
  ppLink.textContent = "Privacy Policy";
@@ -1706,7 +1754,7 @@ function showConsentUI(options) {
1706
1754
  sep1.textContent = "\xB7";
1707
1755
  footer.appendChild(sep1);
1708
1756
  const termsLink = document.createElement("a");
1709
- termsLink.href = `${baseUrl}/terms`;
1757
+ termsLink.href = `${zerocostBaseUrl}/terms`;
1710
1758
  termsLink.target = "_blank";
1711
1759
  termsLink.rel = "noopener noreferrer";
1712
1760
  termsLink.textContent = "Terms";
@@ -1716,7 +1764,7 @@ function showConsentUI(options) {
1716
1764
  sep2.textContent = "\xB7";
1717
1765
  footer.appendChild(sep2);
1718
1766
  const dnsLink = document.createElement("a");
1719
- dnsLink.href = `${baseUrl}/do-not-sell`;
1767
+ dnsLink.href = `${zerocostBaseUrl}/do-not-sell`;
1720
1768
  dnsLink.target = "_blank";
1721
1769
  dnsLink.rel = "noopener noreferrer";
1722
1770
  dnsLink.textContent = "Do Not Sell My Data";
@@ -1760,6 +1808,9 @@ var ConsentManager = class {
1760
1808
  this.appName = opts.appName ?? "";
1761
1809
  this.theme = opts.theme ?? "dark";
1762
1810
  this.hydrateFromStorage();
1811
+ if (this.consentConfig.showSettingsButton) {
1812
+ this.injectSettingsButton();
1813
+ }
1763
1814
  }
1764
1815
  // ── Public API (per spec §6.3) ───────────────────────────────────
1765
1816
  /** Returns the current consent record, or null if none exists. */
@@ -1825,6 +1876,9 @@ var ConsentManager = class {
1825
1876
  };
1826
1877
  this.record = record;
1827
1878
  this.writeStorage(record);
1879
+ if (this.consentConfig.showSettingsButton) {
1880
+ this.injectSettingsButton();
1881
+ }
1828
1882
  if (this.consentConfig.onConsentChange) {
1829
1883
  try {
1830
1884
  this.consentConfig.onConsentChange(record);
@@ -1903,6 +1957,23 @@ var ConsentManager = class {
1903
1957
  this.client.log(`Failed to submit consent to server: ${err}`);
1904
1958
  }
1905
1959
  }
1960
+ injectSettingsButton() {
1961
+ if (typeof document === "undefined") return;
1962
+ const existing = document.getElementById("zerocost-privacy-settings-btn");
1963
+ if (existing) return;
1964
+ const btn = document.createElement("button");
1965
+ btn.id = "zerocost-privacy-settings-btn";
1966
+ btn.setAttribute("aria-label", "Privacy Settings");
1967
+ btn.title = "Privacy Settings";
1968
+ const pos = this.consentConfig.buttonPosition || "bottom-left";
1969
+ btn.className = `zc-settings-btn zc-settings-${pos}`;
1970
+ btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>`;
1971
+ btn.addEventListener("click", () => this.open());
1972
+ document.body.appendChild(btn);
1973
+ }
1974
+ removeSettingsButton() {
1975
+ document.getElementById("zerocost-privacy-settings-btn")?.remove();
1976
+ }
1906
1977
  };
1907
1978
 
1908
1979
  // src/index.ts
@@ -1936,21 +2007,15 @@ var ZerocostSDK = class {
1936
2007
  });
1937
2008
  }
1938
2009
  async init() {
1939
- this.core.init();
1940
2010
  if (typeof document === "undefined") {
1941
- this.core.log("Running in non-browser environment; skipping DOM injection.");
1942
2011
  return;
1943
2012
  }
1944
2013
  if (window !== window.top) {
1945
- this.core.log("Running inside an iframe. Ads render if permissions allow.");
1946
2014
  }
1947
- this.core.log("Initializing Zerocost SDK.");
1948
2015
  if (this.consent.shouldPrompt()) {
1949
- this.core.log("Consent required \u2014 showing prompt.");
1950
2016
  await this.consent.promptAndWait();
1951
2017
  }
1952
2018
  if (!this.consent.has("ads")) {
1953
- this.core.log("Ads consent not granted \u2014 skipping ad injection.");
1954
2019
  return;
1955
2020
  }
1956
2021
  const cachedConfig = this.readCachedConfig();
@@ -1958,12 +2023,11 @@ var ZerocostSDK = class {
1958
2023
  this.lastConfigHash = this.configToHash(cachedConfig);
1959
2024
  this.syncDataCollection(cachedConfig.dataCollection);
1960
2025
  await this.widget.autoInjectWithConfig(cachedConfig.display, cachedConfig.widget);
1961
- this.core.log("Applied cached config immediately.");
1962
2026
  }
1963
2027
  this.startConfigSync();
1964
2028
  try {
1965
2029
  await this.refreshConfig({ force: true, reason: "init" });
1966
- this.core.log("SDK fully initialized. Ads are rendering automatically.");
2030
+ this.core.log(`Zerocost SDK initialized (${this.core.getConfig().appId})`);
1967
2031
  } catch (err) {
1968
2032
  this.core.log(`Init error: ${err}. Attempting fallback ad injection.`);
1969
2033
  await this.widget.autoInject();
@@ -2137,6 +2201,10 @@ var ZerocostSDK = class {
2137
2201
  return { valid: false, error: err.message };
2138
2202
  }
2139
2203
  }
2204
+ /** Open the consent/privacy settings popup. */
2205
+ async showSettings() {
2206
+ return this.consent.open();
2207
+ }
2140
2208
  };
2141
2209
  // Annotate the CommonJS export names for ESM import in node:
2142
2210
  0 && (module.exports = {
package/dist/index.d.ts CHANGED
@@ -39,6 +39,8 @@ export declare class ZerocostSDK {
39
39
  valid: boolean;
40
40
  error?: string;
41
41
  }>;
42
+ /** Open the consent/privacy settings popup. */
43
+ showSettings(): Promise<void>;
42
44
  }
43
45
  export * from './types';
44
46
  export { ZerocostClient } from './core/client';
package/dist/index.js CHANGED
@@ -1,3 +1,11 @@
1
+ // src/core/constants.ts
2
+ var ZEROCOST_DOMAINS = {
3
+ MAIN: "https://zerocost.lovable.app",
4
+ APP: "https://app.zerocost.com",
5
+ DOCS: "https://docs.zerocost.com"
6
+ };
7
+ var ZEROCOST_BASE_URL = ZEROCOST_DOMAINS.MAIN;
8
+
1
9
  // src/core/config.ts
2
10
  var EDGE_FUNCTION_BASE = "https://mwbgzpbuoojqsuxduieo.supabase.co/functions/v1";
3
11
  function getBaseUrl(custom) {
@@ -40,7 +48,7 @@ var ZerocostClient = class {
40
48
  ...body || {},
41
49
  app_id: this.config.appId
42
50
  };
43
- this.log(`\u2192 ${url}`, payload);
51
+ this.log(`\u2192 ${path}`, payload);
44
52
  const res = await fetch(url, {
45
53
  method: "POST",
46
54
  headers: {
@@ -59,7 +67,8 @@ var ZerocostClient = class {
59
67
  }
60
68
  log(message, data) {
61
69
  if (this.config.debug) {
62
- console.log(`[Zerocost] ${message}`, data ?? "");
70
+ const sanitizedMessage = typeof message === "string" ? message.replace(/https:\/\/[a-z0-9.-]+\.supabase\.co/gi, "[INFRA]") : message;
71
+ console.log(`[Zerocost] ${sanitizedMessage}`, data ?? "");
63
72
  }
64
73
  }
65
74
  };
@@ -1324,13 +1333,14 @@ function injectStyles(theme) {
1324
1333
  `;
1325
1334
  }
1326
1335
  const css = `
1336
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
1327
1337
  ${themeRule}
1328
1338
 
1329
1339
  .zc-consent-root * {
1330
1340
  box-sizing: border-box;
1331
1341
  margin: 0;
1332
1342
  padding: 0;
1333
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, sans-serif;
1343
+ font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto, sans-serif;
1334
1344
  }
1335
1345
 
1336
1346
  .zc-consent-backdrop {
@@ -1359,10 +1369,10 @@ function injectStyles(theme) {
1359
1369
  border: 1px solid var(--zc-border);
1360
1370
  border-radius: 16px;
1361
1371
  width: 100%;
1362
- max-width: 460px;
1372
+ max-width: 440px;
1363
1373
  max-height: 90vh;
1364
1374
  overflow-y: auto;
1365
- padding: 28px 24px 24px;
1375
+ padding: 24px 20px 20px;
1366
1376
  animation: zc-fade-in 200ms ease;
1367
1377
  }
1368
1378
 
@@ -1386,7 +1396,7 @@ function injectStyles(theme) {
1386
1396
  display: flex;
1387
1397
  align-items: center;
1388
1398
  gap: 10px;
1389
- margin-bottom: 6px;
1399
+ margin-bottom: 4px;
1390
1400
  }
1391
1401
 
1392
1402
  .zc-consent-logo {
@@ -1416,21 +1426,21 @@ function injectStyles(theme) {
1416
1426
  font-size: 13px;
1417
1427
  color: var(--zc-text-secondary);
1418
1428
  line-height: 1.5;
1419
- margin-bottom: 20px;
1429
+ margin-bottom: 16px;
1420
1430
  }
1421
1431
 
1422
1432
  .zc-consent-toggles {
1423
1433
  display: flex;
1424
1434
  flex-direction: column;
1425
1435
  gap: 10px;
1426
- margin-bottom: 20px;
1436
+ margin-bottom: 16px;
1427
1437
  }
1428
1438
 
1429
1439
  .zc-consent-toggle-card {
1430
1440
  background: var(--zc-surface);
1431
1441
  border: 1px solid var(--zc-border);
1432
1442
  border-radius: 12px;
1433
- padding: 14px 16px;
1443
+ padding: 12px 14px;
1434
1444
  }
1435
1445
 
1436
1446
  .zc-consent-toggle-row {
@@ -1515,7 +1525,7 @@ function injectStyles(theme) {
1515
1525
  flex-wrap: wrap;
1516
1526
  gap: 4px 12px;
1517
1527
  justify-content: center;
1518
- margin-bottom: 16px;
1528
+ margin-bottom: 14px;
1519
1529
  }
1520
1530
 
1521
1531
  .zc-consent-footer a {
@@ -1558,6 +1568,44 @@ function injectStyles(theme) {
1558
1568
  .zc-consent-confirm:active {
1559
1569
  opacity: 0.75;
1560
1570
  }
1571
+
1572
+ /* Floating settings button */
1573
+ .zc-settings-btn {
1574
+ position: fixed;
1575
+ width: 44px;
1576
+ height: 44px;
1577
+ border-radius: 50%;
1578
+ background: var(--zc-bg);
1579
+ border: 1px solid var(--zc-border);
1580
+ color: var(--zc-text);
1581
+ display: flex;
1582
+ align-items: center;
1583
+ justify-content: center;
1584
+ cursor: pointer;
1585
+ z-index: 999998;
1586
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
1587
+ transition: transform 200ms, border-color 200ms;
1588
+ }
1589
+
1590
+ .zc-settings-btn:hover {
1591
+ transform: scale(1.05);
1592
+ border-color: var(--zc-text-secondary);
1593
+ }
1594
+
1595
+ .zc-settings-btn svg {
1596
+ width: 20px;
1597
+ height: 20px;
1598
+ }
1599
+
1600
+ .zc-settings-bottom-left { bottom: 20px; left: 20px; }
1601
+ .zc-settings-bottom-right { bottom: 20px; right: 20px; }
1602
+ .zc-settings-top-left { top: 20px; left: 20px; }
1603
+ .zc-settings-top-right { top: 20px; right: 20px; }
1604
+
1605
+ @media (max-width: 640px) {
1606
+ .zc-settings-btn { width: 40px; height: 40px; }
1607
+ .zc-settings-bottom-left, .zc-settings-bottom-right { bottom: 16px; }
1608
+ }
1561
1609
  `;
1562
1610
  const style = document.createElement("style");
1563
1611
  style.id = STYLE_ID;
@@ -1640,33 +1688,33 @@ function showConsentUI(options) {
1640
1688
  card.appendChild(subtitle);
1641
1689
  const toggles = document.createElement("div");
1642
1690
  toggles.className = "zc-consent-toggles";
1643
- const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
1691
+ const zerocostBaseUrl = ZEROCOST_BASE_URL;
1644
1692
  toggles.appendChild(createToggleCard(
1645
1693
  "zc-toggle-ads",
1646
1694
  "Ads",
1647
1695
  "Contextual, non-intrusive ads. No cookies or browsing history used.",
1648
- `${baseUrl}/consent/ads`,
1696
+ `${zerocostBaseUrl}/consent/ads`,
1649
1697
  defaults.ads
1650
1698
  ));
1651
1699
  toggles.appendChild(createToggleCard(
1652
1700
  "zc-toggle-usage",
1653
1701
  "Usage data",
1654
1702
  "Anonymized usage patterns. No personal information is shared.",
1655
- `${baseUrl}/consent/usage-data`,
1703
+ `${zerocostBaseUrl}/consent/usage-data`,
1656
1704
  defaults.usageData
1657
1705
  ));
1658
1706
  toggles.appendChild(createToggleCard(
1659
1707
  "zc-toggle-ai",
1660
1708
  "AI interactions",
1661
1709
  "Anonymized conversation data used for AI research.",
1662
- `${baseUrl}/consent/ai-interactions`,
1710
+ `${zerocostBaseUrl}/consent/ai-interactions`,
1663
1711
  defaults.aiInteractions
1664
1712
  ));
1665
1713
  card.appendChild(toggles);
1666
1714
  const footer = document.createElement("div");
1667
1715
  footer.className = "zc-consent-footer";
1668
1716
  const ppLink = document.createElement("a");
1669
- ppLink.href = privacyPolicyUrl || `${baseUrl}/privacy`;
1717
+ ppLink.href = privacyPolicyUrl || `${zerocostBaseUrl}/privacy`;
1670
1718
  ppLink.target = "_blank";
1671
1719
  ppLink.rel = "noopener noreferrer";
1672
1720
  ppLink.textContent = "Privacy Policy";
@@ -1676,7 +1724,7 @@ function showConsentUI(options) {
1676
1724
  sep1.textContent = "\xB7";
1677
1725
  footer.appendChild(sep1);
1678
1726
  const termsLink = document.createElement("a");
1679
- termsLink.href = `${baseUrl}/terms`;
1727
+ termsLink.href = `${zerocostBaseUrl}/terms`;
1680
1728
  termsLink.target = "_blank";
1681
1729
  termsLink.rel = "noopener noreferrer";
1682
1730
  termsLink.textContent = "Terms";
@@ -1686,7 +1734,7 @@ function showConsentUI(options) {
1686
1734
  sep2.textContent = "\xB7";
1687
1735
  footer.appendChild(sep2);
1688
1736
  const dnsLink = document.createElement("a");
1689
- dnsLink.href = `${baseUrl}/do-not-sell`;
1737
+ dnsLink.href = `${zerocostBaseUrl}/do-not-sell`;
1690
1738
  dnsLink.target = "_blank";
1691
1739
  dnsLink.rel = "noopener noreferrer";
1692
1740
  dnsLink.textContent = "Do Not Sell My Data";
@@ -1730,6 +1778,9 @@ var ConsentManager = class {
1730
1778
  this.appName = opts.appName ?? "";
1731
1779
  this.theme = opts.theme ?? "dark";
1732
1780
  this.hydrateFromStorage();
1781
+ if (this.consentConfig.showSettingsButton) {
1782
+ this.injectSettingsButton();
1783
+ }
1733
1784
  }
1734
1785
  // ── Public API (per spec §6.3) ───────────────────────────────────
1735
1786
  /** Returns the current consent record, or null if none exists. */
@@ -1795,6 +1846,9 @@ var ConsentManager = class {
1795
1846
  };
1796
1847
  this.record = record;
1797
1848
  this.writeStorage(record);
1849
+ if (this.consentConfig.showSettingsButton) {
1850
+ this.injectSettingsButton();
1851
+ }
1798
1852
  if (this.consentConfig.onConsentChange) {
1799
1853
  try {
1800
1854
  this.consentConfig.onConsentChange(record);
@@ -1873,6 +1927,23 @@ var ConsentManager = class {
1873
1927
  this.client.log(`Failed to submit consent to server: ${err}`);
1874
1928
  }
1875
1929
  }
1930
+ injectSettingsButton() {
1931
+ if (typeof document === "undefined") return;
1932
+ const existing = document.getElementById("zerocost-privacy-settings-btn");
1933
+ if (existing) return;
1934
+ const btn = document.createElement("button");
1935
+ btn.id = "zerocost-privacy-settings-btn";
1936
+ btn.setAttribute("aria-label", "Privacy Settings");
1937
+ btn.title = "Privacy Settings";
1938
+ const pos = this.consentConfig.buttonPosition || "bottom-left";
1939
+ btn.className = `zc-settings-btn zc-settings-${pos}`;
1940
+ btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>`;
1941
+ btn.addEventListener("click", () => this.open());
1942
+ document.body.appendChild(btn);
1943
+ }
1944
+ removeSettingsButton() {
1945
+ document.getElementById("zerocost-privacy-settings-btn")?.remove();
1946
+ }
1876
1947
  };
1877
1948
 
1878
1949
  // src/index.ts
@@ -1906,21 +1977,15 @@ var ZerocostSDK = class {
1906
1977
  });
1907
1978
  }
1908
1979
  async init() {
1909
- this.core.init();
1910
1980
  if (typeof document === "undefined") {
1911
- this.core.log("Running in non-browser environment; skipping DOM injection.");
1912
1981
  return;
1913
1982
  }
1914
1983
  if (window !== window.top) {
1915
- this.core.log("Running inside an iframe. Ads render if permissions allow.");
1916
1984
  }
1917
- this.core.log("Initializing Zerocost SDK.");
1918
1985
  if (this.consent.shouldPrompt()) {
1919
- this.core.log("Consent required \u2014 showing prompt.");
1920
1986
  await this.consent.promptAndWait();
1921
1987
  }
1922
1988
  if (!this.consent.has("ads")) {
1923
- this.core.log("Ads consent not granted \u2014 skipping ad injection.");
1924
1989
  return;
1925
1990
  }
1926
1991
  const cachedConfig = this.readCachedConfig();
@@ -1928,12 +1993,11 @@ var ZerocostSDK = class {
1928
1993
  this.lastConfigHash = this.configToHash(cachedConfig);
1929
1994
  this.syncDataCollection(cachedConfig.dataCollection);
1930
1995
  await this.widget.autoInjectWithConfig(cachedConfig.display, cachedConfig.widget);
1931
- this.core.log("Applied cached config immediately.");
1932
1996
  }
1933
1997
  this.startConfigSync();
1934
1998
  try {
1935
1999
  await this.refreshConfig({ force: true, reason: "init" });
1936
- this.core.log("SDK fully initialized. Ads are rendering automatically.");
2000
+ this.core.log(`Zerocost SDK initialized (${this.core.getConfig().appId})`);
1937
2001
  } catch (err) {
1938
2002
  this.core.log(`Init error: ${err}. Attempting fallback ad injection.`);
1939
2003
  await this.widget.autoInject();
@@ -2107,6 +2171,10 @@ var ZerocostSDK = class {
2107
2171
  return { valid: false, error: err.message };
2108
2172
  }
2109
2173
  }
2174
+ /** Open the consent/privacy settings popup. */
2175
+ async showSettings() {
2176
+ return this.consent.open();
2177
+ }
2110
2178
  };
2111
2179
  export {
2112
2180
  ConsentManager,
@@ -11,6 +11,8 @@ export interface ZerocostConsentRecord {
11
11
  }
12
12
  export interface ConsentConfig {
13
13
  privacyPolicyUrl?: string;
14
+ showSettingsButton?: boolean;
15
+ buttonPosition?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
14
16
  onConsentChange?: (preferences: ZerocostConsentRecord) => void;
15
17
  }
16
18
  export type ConsentFeature = 'ads' | 'usageData' | 'aiInteractions';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerocost/sdk",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",