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