@contentcredits/sdk 2.0.4 → 2.2.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,5 +1,5 @@
1
1
  function resolveConfig(raw) {
2
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
2
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
3
3
  if (!raw.apiKey || typeof raw.apiKey !== 'string' || raw.apiKey.trim() === '') {
4
4
  throw new Error('[ContentCredits] apiKey is required. Get yours from the Content Credits admin panel.');
5
5
  }
@@ -8,7 +8,7 @@ function resolveConfig(raw) {
8
8
  try {
9
9
  hostName = new URL(articleUrl).hostname;
10
10
  }
11
- catch (_l) {
11
+ catch (_m) {
12
12
  throw new Error(`[ContentCredits] Invalid articleUrl: "${articleUrl}"`);
13
13
  }
14
14
  return {
@@ -21,13 +21,14 @@ function resolveConfig(raw) {
21
21
  enableComments: (_d = raw.enableComments) !== null && _d !== void 0 ? _d : true,
22
22
  extensionId: (_e = raw.extensionId) !== null && _e !== void 0 ? _e : "ljehdpabbhgccmanhjdfacjnaigpgcml",
23
23
  debug: (_f = raw.debug) !== null && _f !== void 0 ? _f : false,
24
+ headless: (_g = raw.headless) !== null && _g !== void 0 ? _g : false,
24
25
  apiBaseUrl: "https://api.contentcredits.com",
25
26
  accountsUrl: "https://accounts.contentcredits.com",
26
27
  paywallTemplate: raw.paywallTemplate,
27
28
  onAccessGranted: raw.onAccessGranted,
28
29
  theme: {
29
- primaryColor: (_h = (_g = raw.theme) === null || _g === void 0 ? void 0 : _g.primaryColor) !== null && _h !== void 0 ? _h : '#44C678',
30
- fontFamily: (_k = (_j = raw.theme) === null || _j === void 0 ? void 0 : _j.fontFamily) !== null && _k !== void 0 ? _k : "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
30
+ primaryColor: (_j = (_h = raw.theme) === null || _h === void 0 ? void 0 : _h.primaryColor) !== null && _j !== void 0 ? _j : '#44C678',
31
+ fontFamily: (_l = (_k = raw.theme) === null || _k === void 0 ? void 0 : _k.fontFamily) !== null && _l !== void 0 ? _l : "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
31
32
  },
32
33
  };
33
34
  }
@@ -518,6 +519,20 @@ function createGate(options) {
518
519
  }
519
520
  contentEl.setAttribute(GATE_ATTR, 'true');
520
521
  gated = true;
522
+ // Add a gradient fade at the bottom of the visible teaser so the cutoff
523
+ // looks intentional rather than abrupt.
524
+ if (!contentEl.querySelector('[data-cc-fade]')) {
525
+ const prevPos = contentEl.style.position;
526
+ if (!prevPos || prevPos === 'static')
527
+ contentEl.style.position = 'relative';
528
+ const fadeEl = document.createElement('div');
529
+ fadeEl.setAttribute('data-cc-fade', 'true');
530
+ fadeEl.style.cssText =
531
+ 'position:absolute;bottom:0;left:0;width:100%;height:160px;' +
532
+ 'background:linear-gradient(to bottom,transparent 0%,var(--cc-bg,#fff) 100%);' +
533
+ 'pointer-events:none;z-index:1;';
534
+ contentEl.appendChild(fadeEl);
535
+ }
521
536
  return true;
522
537
  }
523
538
  function reveal() {
@@ -529,6 +544,10 @@ function createGate(options) {
529
544
  n.removeAttribute('data-cc-hidden');
530
545
  }
531
546
  });
547
+ // Remove gradient fade
548
+ const fadeEl = contentEl === null || contentEl === void 0 ? void 0 : contentEl.querySelector('[data-cc-fade]');
549
+ if (fadeEl)
550
+ fadeEl.remove();
532
551
  hiddenNodes = [];
533
552
  contentEl === null || contentEl === void 0 ? void 0 : contentEl.removeAttribute(GATE_ATTR);
534
553
  gated = false;
@@ -561,6 +580,24 @@ function createShadowHost(id) {
561
580
  host._ccShadow = root;
562
581
  return { host, root };
563
582
  }
583
+ /**
584
+ * Creates a shadow host inserted immediately after `anchorEl` in the DOM.
585
+ * Used for the inline paywall panel so it flows naturally below the content.
586
+ */
587
+ function createInlineShadowHost(id, anchorEl) {
588
+ let host = document.getElementById(id);
589
+ if (!host) {
590
+ host = document.createElement('div');
591
+ host.id = id;
592
+ anchorEl.parentNode.insertBefore(host, anchorEl.nextSibling);
593
+ }
594
+ const existing = host._ccShadow;
595
+ if (existing)
596
+ return { host, root: existing };
597
+ const root = host.attachShadow({ mode: 'open' });
598
+ host._ccShadow = root;
599
+ return { host, root };
600
+ }
564
601
  function removeShadowHost(id) {
565
602
  const host = document.getElementById(id);
566
603
  if (host)
@@ -583,56 +620,27 @@ function getPaywallStyles(primaryColor, fontFamily) {
583
620
  return `
584
621
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
585
622
 
586
- .cc-paywall-gate {
587
- position: relative;
623
+ /* Inline paywall panel — sits below the teaser content in the page flow */
624
+ .cc-paywall-inline {
588
625
  width: 100%;
589
- }
590
-
591
- .cc-teaser-fade {
592
- position: relative;
593
- }
594
- .cc-teaser-fade::after {
595
- content: '';
596
- position: absolute;
597
- bottom: 0; left: 0; width: 100%;
598
- height: 120px;
599
- background: linear-gradient(to bottom, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
600
- pointer-events: none;
601
- }
602
-
603
- /* Full-viewport backdrop — centers the overlay and blocks background clicks */
604
- .cc-paywall-backdrop {
605
- position: fixed;
606
- inset: 0;
607
- background: rgba(0, 0, 0, 0.45);
608
- display: flex;
609
- align-items: center;
610
- justify-content: center;
611
- padding: 20px;
612
- pointer-events: all;
613
- }
614
-
615
- .cc-paywall-overlay {
626
+ padding: 36px 24px 32px;
616
627
  background: #fff;
617
628
  border: 1px solid #e5e7eb;
618
- border-radius: 12px;
619
- padding: 32px 24px;
620
- width: 100%;
621
- max-width: 480px;
629
+ border-top: 3px solid ${primaryColor};
630
+ border-radius: 0 0 12px 12px;
622
631
  text-align: center;
623
632
  font-family: ${fontFamily};
624
- box-shadow: 0 8px 40px rgba(0,0,0,0.18);
625
- pointer-events: all;
633
+ box-sizing: border-box;
626
634
  }
627
635
 
628
- .cc-paywall-overlay h2 {
636
+ .cc-paywall-inline h2 {
629
637
  font-size: 20px;
630
638
  font-weight: 700;
631
639
  color: #111827;
632
640
  margin-bottom: 8px;
633
641
  }
634
642
 
635
- .cc-paywall-overlay p {
643
+ .cc-paywall-inline p {
636
644
  font-size: 14px;
637
645
  color: #6b7280;
638
646
  margin-bottom: 24px;
@@ -1165,20 +1173,24 @@ function createPaywallRenderer(config) {
1165
1173
  let root = null;
1166
1174
  let overlay = null;
1167
1175
  function init() {
1168
- const { root: shadowRoot } = createShadowHost(HOST_ID);
1176
+ // Insert the shadow host immediately after the content element so the
1177
+ // paywall panel flows inline below the teaser — no modal, no backdrop.
1178
+ const contentEl = document.querySelector(config.contentSelector);
1179
+ if (!contentEl)
1180
+ return;
1181
+ const { root: shadowRoot } = createInlineShadowHost(HOST_ID, contentEl);
1169
1182
  root = shadowRoot;
1170
1183
  injectStyles(root, getPaywallStyles(config.theme.primaryColor, config.theme.fontFamily));
1171
- // Backdrop: fixed full-viewport flex container — centers the overlay and
1172
- // blocks interaction with the page behind it (pointer-events: all in CSS).
1173
- const backdrop = el('div');
1174
- backdrop.className = 'cc-paywall-backdrop';
1175
- root.appendChild(backdrop);
1176
1184
  overlay = el('div');
1177
- overlay.className = 'cc-paywall-overlay';
1178
- backdrop.appendChild(overlay);
1185
+ overlay.className = 'cc-paywall-inline';
1186
+ root.appendChild(overlay);
1179
1187
  }
1180
1188
  function render(state, callbacks, meta) {
1181
1189
  var _a, _b, _c;
1190
+ // During the initial access check the content is already hidden by the
1191
+ // gate — no need to show a spinner in the paywall slot.
1192
+ if (state === 'checking')
1193
+ return;
1182
1194
  if (!overlay)
1183
1195
  init();
1184
1196
  if (!overlay)
@@ -1187,9 +1199,6 @@ function createPaywallRenderer(config) {
1187
1199
  while (overlay.firstChild)
1188
1200
  overlay.removeChild(overlay.firstChild);
1189
1201
  switch (state) {
1190
- case 'checking':
1191
- renderChecking(overlay);
1192
- break;
1193
1202
  case 'login':
1194
1203
  renderLogin(overlay, callbacks);
1195
1204
  break;
@@ -1207,20 +1216,6 @@ function createPaywallRenderer(config) {
1207
1216
  break;
1208
1217
  }
1209
1218
  }
1210
- function renderChecking(parent) {
1211
- const spinner = el('div');
1212
- spinner.className = 'cc-spinner';
1213
- spinner.style.margin = '0 auto 12px';
1214
- spinner.style.width = '24px';
1215
- spinner.style.height = '24px';
1216
- spinner.style.borderWidth = '2px';
1217
- spinner.style.borderColor = '#e5e7eb';
1218
- spinner.style.borderTopColor = config.theme.primaryColor;
1219
- const text = el('p', 'Checking access...');
1220
- text.style.cssText = 'font-size:14px;color:#6b7280;text-align:center;font-family:' + config.theme.fontFamily;
1221
- parent.appendChild(spinner);
1222
- parent.appendChild(text);
1223
- }
1224
1219
  function renderLogin(parent, cb) {
1225
1220
  parent.appendChild(el('h2', 'This article requires a subscription'));
1226
1221
  parent.appendChild(el('p', 'Log in with your Content Credits account to unlock this article.'));
@@ -1611,8 +1606,10 @@ function openAuthPopup(authUrl) {
1611
1606
  });
1612
1607
  }
1613
1608
 
1614
- function createPaywall(config, creditsApi, state, emitter) {
1615
- const gate = createGate({
1609
+ function createPaywall(config, creditsApi, state, emitter, existingGate) {
1610
+ // Accept a pre-created gate so the caller can call gate.hide() synchronously
1611
+ // before any async work, preventing a flash of the full article content.
1612
+ const gate = existingGate !== null && existingGate !== void 0 ? existingGate : createGate({
1616
1613
  selector: config.contentSelector,
1617
1614
  teaserParagraphs: config.teaserParagraphs,
1618
1615
  });
@@ -1627,8 +1624,10 @@ function createPaywall(config, creditsApi, state, emitter) {
1627
1624
  function handleAccessGranted(creditsSpent = 0, balance = 0) {
1628
1625
  var _a;
1629
1626
  state.set({ hasAccess: true, isLoaded: true, isLoading: false });
1630
- gate.reveal();
1631
- renderer.render('granted', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1627
+ if (!config.headless) {
1628
+ gate.reveal();
1629
+ renderer.render('granted', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1630
+ }
1632
1631
  emitter.emit('paywall:hidden', {});
1633
1632
  emitter.emit('article:purchased', { creditsSpent, remainingBalance: balance });
1634
1633
  (_a = config.onAccessGranted) === null || _a === void 0 ? void 0 : _a.call(config);
@@ -1645,7 +1644,8 @@ function createPaywall(config, creditsApi, state, emitter) {
1645
1644
  window.location.href = authUrl;
1646
1645
  return;
1647
1646
  }
1648
- renderer.render('loading', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1647
+ if (!config.headless)
1648
+ renderer.render('loading', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1649
1649
  const token = await openAuthPopup(authUrl);
1650
1650
  if (token) {
1651
1651
  state.set({ isLoggedIn: true });
@@ -1653,7 +1653,8 @@ function createPaywall(config, creditsApi, state, emitter) {
1653
1653
  }
1654
1654
  else {
1655
1655
  // Popup closed without login
1656
- renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1656
+ if (!config.headless)
1657
+ renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1657
1658
  }
1658
1659
  }
1659
1660
  // ── Purchase ──────────────────────────────────────────────────────────────
@@ -1672,7 +1673,8 @@ function createPaywall(config, creditsApi, state, emitter) {
1672
1673
  });
1673
1674
  return;
1674
1675
  }
1675
- renderer.render('loading', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1676
+ if (!config.headless)
1677
+ renderer.render('loading', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1676
1678
  state.set({ isLoading: true });
1677
1679
  try {
1678
1680
  const result = await creditsApi.purchaseArticle({
@@ -1686,7 +1688,8 @@ function createPaywall(config, creditsApi, state, emitter) {
1686
1688
  }
1687
1689
  else {
1688
1690
  state.set({ isLoading: false });
1689
- renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1691
+ if (!config.headless)
1692
+ renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1690
1693
  emitter.emit('error', { message: (_a = result.message) !== null && _a !== void 0 ? _a : 'Purchase failed' });
1691
1694
  }
1692
1695
  }
@@ -1694,17 +1697,20 @@ function createPaywall(config, creditsApi, state, emitter) {
1694
1697
  state.set({ isLoading: false });
1695
1698
  if (err instanceof ApiError && err.status === 402) {
1696
1699
  // Insufficient credits
1697
- renderer.render('insufficient', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits }, {
1698
- requiredCredits: state.get().requiredCredits,
1699
- creditBalance: state.get().creditBalance,
1700
- });
1700
+ if (!config.headless) {
1701
+ renderer.render('insufficient', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits }, {
1702
+ requiredCredits: state.get().requiredCredits,
1703
+ creditBalance: state.get().creditBalance,
1704
+ });
1705
+ }
1701
1706
  emitter.emit('credits:insufficient', {
1702
1707
  required: (_b = state.get().requiredCredits) !== null && _b !== void 0 ? _b : 0,
1703
1708
  available: (_c = state.get().creditBalance) !== null && _c !== void 0 ? _c : 0,
1704
1709
  });
1705
1710
  }
1706
1711
  else {
1707
- renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1712
+ if (!config.headless)
1713
+ renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1708
1714
  emitter.emit('error', { message: 'Purchase failed', error: err });
1709
1715
  }
1710
1716
  }
@@ -1715,15 +1721,18 @@ function createPaywall(config, creditsApi, state, emitter) {
1715
1721
  // ── Access Check ──────────────────────────────────────────────────────────
1716
1722
  async function checkAccess() {
1717
1723
  state.set({ isLoading: true });
1718
- renderer.render('checking', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1724
+ if (!config.headless)
1725
+ renderer.render('checking', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1719
1726
  if (extensionAvailable) {
1720
1727
  bridge.requestAuthorization(config.apiKey, config.hostName);
1721
1728
  return; // response handled in onAuthorizationResponse
1722
1729
  }
1723
1730
  if (!tokenStorage.has()) {
1724
1731
  state.set({ isLoading: false, isLoaded: true });
1725
- gate.hide();
1726
- renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1732
+ if (!config.headless) {
1733
+ gate.hide();
1734
+ renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1735
+ }
1727
1736
  emitter.emit('paywall:shown', {});
1728
1737
  return;
1729
1738
  }
@@ -1743,15 +1752,19 @@ function createPaywall(config, creditsApi, state, emitter) {
1743
1752
  handleAccessGranted(0, 0);
1744
1753
  }
1745
1754
  else {
1746
- gate.hide();
1747
- renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1755
+ if (!config.headless) {
1756
+ gate.hide();
1757
+ renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1758
+ }
1748
1759
  emitter.emit('paywall:shown', {});
1749
1760
  }
1750
1761
  }
1751
1762
  catch (err) {
1752
1763
  state.set({ isLoading: false, isLoaded: true });
1753
- gate.hide();
1754
- renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1764
+ if (!config.headless) {
1765
+ gate.hide();
1766
+ renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1767
+ }
1755
1768
  if (!(err instanceof ApiError && err.status === 401)) {
1756
1769
  emitter.emit('error', { message: 'Access check failed', error: err });
1757
1770
  }
@@ -1780,19 +1793,23 @@ function createPaywall(config, creditsApi, state, emitter) {
1780
1793
  requiredCredits: (_b = data.requiredCredits) !== null && _b !== void 0 ? _b : null,
1781
1794
  });
1782
1795
  if (!data.isAuthenticated) {
1783
- gate.hide();
1784
- renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1796
+ if (!config.headless) {
1797
+ gate.hide();
1798
+ renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1799
+ }
1785
1800
  emitter.emit('paywall:shown', {});
1786
1801
  }
1787
1802
  else if (data.doesHaveAccess) {
1788
1803
  handleAccessGranted(0, (_c = data.creditBalance) !== null && _c !== void 0 ? _c : 0);
1789
1804
  }
1790
1805
  else {
1791
- gate.hide();
1792
- renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits }, {
1793
- requiredCredits: data.requiredCredits,
1794
- creditBalance: data.creditBalance,
1795
- });
1806
+ if (!config.headless) {
1807
+ gate.hide();
1808
+ renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits }, {
1809
+ requiredCredits: data.requiredCredits,
1810
+ creditBalance: data.creditBalance,
1811
+ });
1812
+ }
1796
1813
  emitter.emit('paywall:shown', {});
1797
1814
  }
1798
1815
  });
@@ -1812,10 +1829,19 @@ function createPaywall(config, creditsApi, state, emitter) {
1812
1829
  }
1813
1830
  function destroy() {
1814
1831
  bridge.detach();
1815
- renderer.destroy();
1816
- gate.reveal();
1832
+ if (!config.headless) {
1833
+ renderer.destroy();
1834
+ gate.reveal();
1835
+ }
1817
1836
  }
1818
- return { init, checkAccess, destroy };
1837
+ return {
1838
+ init,
1839
+ checkAccess,
1840
+ destroy,
1841
+ login: doLogin,
1842
+ purchase: doPurchase,
1843
+ buyMoreCredits: doBuyMoreCredits,
1844
+ };
1819
1845
  }
1820
1846
 
1821
1847
  const POSITION_KEY = 'cc-widget-pos';
@@ -2677,14 +2703,25 @@ class ContentCredits {
2677
2703
  async _start() {
2678
2704
  // 1. Consume any token that arrived in the URL (mobile redirect flow)
2679
2705
  consumeTokenFromUrl();
2680
- // 2. If no access token in memory/session, attempt a silent refresh.
2706
+ // 2. Hide premium content immediately (synchronous) before any async work.
2707
+ // This prevents the flash of full article content that would otherwise
2708
+ // appear during the token-refresh and access-check network round-trips.
2709
+ // Skipped in headless mode — the host app owns all DOM manipulation.
2710
+ const earlyGate = createGate({
2711
+ selector: this.config.contentSelector,
2712
+ teaserParagraphs: this.config.teaserParagraphs,
2713
+ });
2714
+ if (!this.config.headless)
2715
+ earlyGate.hide();
2716
+ // 3. If no access token in memory/session, attempt a silent refresh.
2681
2717
  // This runs on every new browser session (after the browser was closed)
2682
- // and silently re-authenticates the user using their stored refresh token
2683
- // — no popup, no visible delay, no paywall flash.
2718
+ // and silently re-authenticates the user using their stored refresh token.
2684
2719
  if (!tokenStorage.has()) {
2685
2720
  await tryRefreshSession(this.config.apiBaseUrl);
2686
2721
  }
2687
- this.paywallModule = createPaywall(this.config, this.creditsApi, this.state, this.emitter);
2722
+ // Pass the pre-created gate so createPaywall reuses the same instance
2723
+ // (and its hiddenNodes list) rather than creating a second one.
2724
+ this.paywallModule = createPaywall(this.config, this.creditsApi, this.state, this.emitter, earlyGate);
2688
2725
  if (this.config.enableComments) {
2689
2726
  this.commentsModule = createComments(this.config, this.commentsApi, this.emitter);
2690
2727
  this.commentsModule.init();
@@ -2693,6 +2730,63 @@ class ContentCredits {
2693
2730
  this.emitter.emit('ready', { state: this.state.get() });
2694
2731
  }
2695
2732
  // ── Public API ────────────────────────────────────────────────────────────
2733
+ /**
2734
+ * Subscribe to state changes. The callback receives the full state snapshot
2735
+ * every time any field changes. Returns an unsubscribe function.
2736
+ *
2737
+ * Primarily useful in **headless mode** — lets you drive your own UI from
2738
+ * reactive state without polling `getState()`.
2739
+ *
2740
+ * @example
2741
+ * const unsubscribe = cc.subscribe((state) => {
2742
+ * if (state.hasAccess) showFullContent();
2743
+ * else showPaywall(state);
2744
+ * });
2745
+ */
2746
+ subscribe(fn) {
2747
+ return this.state.subscribe(fn);
2748
+ }
2749
+ /**
2750
+ * Trigger the login flow programmatically.
2751
+ *
2752
+ * - Desktop: opens a popup window to the Content Credits auth page.
2753
+ * - Mobile: performs a full-page redirect.
2754
+ * - Extension: delegates to the browser extension.
2755
+ *
2756
+ * Primarily useful in **headless mode** where you render your own "Login"
2757
+ * button and call this from its `onClick` handler.
2758
+ */
2759
+ async login() {
2760
+ var _a;
2761
+ await ((_a = this.paywallModule) === null || _a === void 0 ? void 0 : _a.login());
2762
+ }
2763
+ /**
2764
+ * Trigger the article purchase flow programmatically.
2765
+ *
2766
+ * Deducts the required credits from the user's balance and, on success,
2767
+ * updates `state.hasAccess` to `true` and emits `article:purchased`.
2768
+ *
2769
+ * If the user is not logged in, this automatically opens the login flow
2770
+ * first, then proceeds with the purchase.
2771
+ *
2772
+ * Primarily useful in **headless mode** where you render your own "Unlock"
2773
+ * button and call this from its `onClick` handler.
2774
+ */
2775
+ async purchase() {
2776
+ var _a;
2777
+ await ((_a = this.paywallModule) === null || _a === void 0 ? void 0 : _a.purchase());
2778
+ }
2779
+ /**
2780
+ * Open the Content Credits dashboard in a new tab so the user can top up
2781
+ * their credit balance.
2782
+ *
2783
+ * Primarily useful in **headless mode** when `state.creditBalance` is lower
2784
+ * than `state.requiredCredits`.
2785
+ */
2786
+ buyMoreCredits() {
2787
+ var _a;
2788
+ (_a = this.paywallModule) === null || _a === void 0 ? void 0 : _a.buyMoreCredits();
2789
+ }
2696
2790
  /** Subscribe to an SDK event. Returns an unsubscribe function. */
2697
2791
  on(event, handler) {
2698
2792
  return this.emitter.on(event, handler);
@@ -2734,7 +2828,7 @@ class ContentCredits {
2734
2828
  }
2735
2829
  /** SDK version string. */
2736
2830
  static get version() {
2737
- return "2.0.4";
2831
+ return "2.2.0";
2738
2832
  }
2739
2833
  }
2740
2834
  // ── Auto-init from script data attributes (CDN usage) ────────────────────────