@contentcredits/sdk 2.1.0 → 2.3.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,23 @@ 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,
31
+ onStateChange: raw.onStateChange,
32
+ onReady: raw.onReady,
33
+ onLoginRequired: raw.onLoginRequired,
34
+ onPurchaseRequired: raw.onPurchaseRequired,
35
+ onInsufficientCredits: raw.onInsufficientCredits,
36
+ onPurchased: raw.onPurchased,
37
+ onUserLogin: raw.onUserLogin,
38
+ onUserLogout: raw.onUserLogout,
39
+ onError: raw.onError,
30
40
  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",
41
+ primaryColor: (_j = (_h = raw.theme) === null || _h === void 0 ? void 0 : _h.primaryColor) !== null && _j !== void 0 ? _j : '#44C678',
42
+ 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
43
  },
34
44
  };
35
45
  }
@@ -1625,8 +1635,10 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
1625
1635
  function handleAccessGranted(creditsSpent = 0, balance = 0) {
1626
1636
  var _a;
1627
1637
  state.set({ hasAccess: true, isLoaded: true, isLoading: false });
1628
- gate.reveal();
1629
- renderer.render('granted', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1638
+ if (!config.headless) {
1639
+ gate.reveal();
1640
+ renderer.render('granted', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1641
+ }
1630
1642
  emitter.emit('paywall:hidden', {});
1631
1643
  emitter.emit('article:purchased', { creditsSpent, remainingBalance: balance });
1632
1644
  (_a = config.onAccessGranted) === null || _a === void 0 ? void 0 : _a.call(config);
@@ -1643,7 +1655,8 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
1643
1655
  window.location.href = authUrl;
1644
1656
  return;
1645
1657
  }
1646
- renderer.render('loading', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1658
+ if (!config.headless)
1659
+ renderer.render('loading', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1647
1660
  const token = await openAuthPopup(authUrl);
1648
1661
  if (token) {
1649
1662
  state.set({ isLoggedIn: true });
@@ -1651,12 +1664,13 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
1651
1664
  }
1652
1665
  else {
1653
1666
  // Popup closed without login
1654
- renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1667
+ if (!config.headless)
1668
+ renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1655
1669
  }
1656
1670
  }
1657
1671
  // ── Purchase ──────────────────────────────────────────────────────────────
1658
1672
  async function doPurchase() {
1659
- var _a, _b, _c;
1673
+ var _a, _b, _c, _d, _e;
1660
1674
  if (!tokenStorage.has()) {
1661
1675
  await doLogin();
1662
1676
  return;
@@ -1670,7 +1684,8 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
1670
1684
  });
1671
1685
  return;
1672
1686
  }
1673
- renderer.render('loading', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1687
+ if (!config.headless)
1688
+ renderer.render('loading', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1674
1689
  state.set({ isLoading: true });
1675
1690
  try {
1676
1691
  const result = await creditsApi.purchaseArticle({
@@ -1684,7 +1699,8 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
1684
1699
  }
1685
1700
  else {
1686
1701
  state.set({ isLoading: false });
1687
- renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1702
+ if (!config.headless)
1703
+ renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1688
1704
  emitter.emit('error', { message: (_a = result.message) !== null && _a !== void 0 ? _a : 'Purchase failed' });
1689
1705
  }
1690
1706
  }
@@ -1692,17 +1708,24 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
1692
1708
  state.set({ isLoading: false });
1693
1709
  if (err instanceof ApiError && err.status === 402) {
1694
1710
  // Insufficient credits
1695
- renderer.render('insufficient', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits }, {
1711
+ if (!config.headless) {
1712
+ renderer.render('insufficient', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits }, {
1713
+ requiredCredits: state.get().requiredCredits,
1714
+ creditBalance: state.get().creditBalance,
1715
+ });
1716
+ }
1717
+ const required = (_b = state.get().requiredCredits) !== null && _b !== void 0 ? _b : 0;
1718
+ const available = (_c = state.get().creditBalance) !== null && _c !== void 0 ? _c : 0;
1719
+ (_d = config.onInsufficientCredits) === null || _d === void 0 ? void 0 : _d.call(config, { required, available });
1720
+ emitter.emit('credits:insufficient', { required, available });
1721
+ }
1722
+ else {
1723
+ if (!config.headless)
1724
+ renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1725
+ (_e = config.onPurchaseRequired) === null || _e === void 0 ? void 0 : _e.call(config, {
1696
1726
  requiredCredits: state.get().requiredCredits,
1697
1727
  creditBalance: state.get().creditBalance,
1698
1728
  });
1699
- emitter.emit('credits:insufficient', {
1700
- required: (_b = state.get().requiredCredits) !== null && _b !== void 0 ? _b : 0,
1701
- available: (_c = state.get().creditBalance) !== null && _c !== void 0 ? _c : 0,
1702
- });
1703
- }
1704
- else {
1705
- renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1706
1729
  emitter.emit('error', { message: 'Purchase failed', error: err });
1707
1730
  }
1708
1731
  }
@@ -1712,16 +1735,21 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
1712
1735
  }
1713
1736
  // ── Access Check ──────────────────────────────────────────────────────────
1714
1737
  async function checkAccess() {
1738
+ var _a, _b, _c;
1715
1739
  state.set({ isLoading: true });
1716
- renderer.render('checking', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1740
+ if (!config.headless)
1741
+ renderer.render('checking', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1717
1742
  if (extensionAvailable) {
1718
1743
  bridge.requestAuthorization(config.apiKey, config.hostName);
1719
1744
  return; // response handled in onAuthorizationResponse
1720
1745
  }
1721
1746
  if (!tokenStorage.has()) {
1722
1747
  state.set({ isLoading: false, isLoaded: true });
1723
- gate.hide();
1724
- renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1748
+ if (!config.headless) {
1749
+ gate.hide();
1750
+ renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1751
+ }
1752
+ (_a = config.onLoginRequired) === null || _a === void 0 ? void 0 : _a.call(config);
1725
1753
  emitter.emit('paywall:shown', {});
1726
1754
  return;
1727
1755
  }
@@ -1741,15 +1769,24 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
1741
1769
  handleAccessGranted(0, 0);
1742
1770
  }
1743
1771
  else {
1744
- gate.hide();
1745
- renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1772
+ if (!config.headless) {
1773
+ gate.hide();
1774
+ renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1775
+ }
1776
+ (_b = config.onPurchaseRequired) === null || _b === void 0 ? void 0 : _b.call(config, {
1777
+ requiredCredits: state.get().requiredCredits,
1778
+ creditBalance: state.get().creditBalance,
1779
+ });
1746
1780
  emitter.emit('paywall:shown', {});
1747
1781
  }
1748
1782
  }
1749
1783
  catch (err) {
1750
1784
  state.set({ isLoading: false, isLoaded: true });
1751
- gate.hide();
1752
- renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1785
+ if (!config.headless) {
1786
+ gate.hide();
1787
+ renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1788
+ }
1789
+ (_c = config.onLoginRequired) === null || _c === void 0 ? void 0 : _c.call(config);
1753
1790
  if (!(err instanceof ApiError && err.status === 401)) {
1754
1791
  emitter.emit('error', { message: 'Access check failed', error: err });
1755
1792
  }
@@ -1768,7 +1805,7 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
1768
1805
  if (extensionAvailable) {
1769
1806
  bridge.attach();
1770
1807
  bridge.onAuthorizationResponse(data => {
1771
- var _a, _b, _c;
1808
+ var _a, _b, _c, _d, _e, _f, _g;
1772
1809
  state.set({
1773
1810
  isLoggedIn: data.isAuthenticated,
1774
1811
  hasAccess: data.doesHaveAccess,
@@ -1778,18 +1815,27 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
1778
1815
  requiredCredits: (_b = data.requiredCredits) !== null && _b !== void 0 ? _b : null,
1779
1816
  });
1780
1817
  if (!data.isAuthenticated) {
1781
- gate.hide();
1782
- renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1818
+ if (!config.headless) {
1819
+ gate.hide();
1820
+ renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1821
+ }
1822
+ (_c = config.onLoginRequired) === null || _c === void 0 ? void 0 : _c.call(config);
1783
1823
  emitter.emit('paywall:shown', {});
1784
1824
  }
1785
1825
  else if (data.doesHaveAccess) {
1786
- handleAccessGranted(0, (_c = data.creditBalance) !== null && _c !== void 0 ? _c : 0);
1826
+ handleAccessGranted(0, (_d = data.creditBalance) !== null && _d !== void 0 ? _d : 0);
1787
1827
  }
1788
1828
  else {
1789
- gate.hide();
1790
- renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits }, {
1791
- requiredCredits: data.requiredCredits,
1792
- creditBalance: data.creditBalance,
1829
+ if (!config.headless) {
1830
+ gate.hide();
1831
+ renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits }, {
1832
+ requiredCredits: data.requiredCredits,
1833
+ creditBalance: data.creditBalance,
1834
+ });
1835
+ }
1836
+ (_e = config.onPurchaseRequired) === null || _e === void 0 ? void 0 : _e.call(config, {
1837
+ requiredCredits: (_f = data.requiredCredits) !== null && _f !== void 0 ? _f : null,
1838
+ creditBalance: (_g = data.creditBalance) !== null && _g !== void 0 ? _g : null,
1793
1839
  });
1794
1840
  emitter.emit('paywall:shown', {});
1795
1841
  }
@@ -1810,10 +1856,19 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
1810
1856
  }
1811
1857
  function destroy() {
1812
1858
  bridge.detach();
1813
- renderer.destroy();
1814
- gate.reveal();
1859
+ if (!config.headless) {
1860
+ renderer.destroy();
1861
+ gate.reveal();
1862
+ }
1815
1863
  }
1816
- return { init, checkAccess, destroy };
1864
+ return {
1865
+ init,
1866
+ checkAccess,
1867
+ destroy,
1868
+ login: doLogin,
1869
+ purchase: doPurchase,
1870
+ buyMoreCredits: doBuyMoreCredits,
1871
+ };
1817
1872
  }
1818
1873
 
1819
1874
  const POSITION_KEY = 'cc-widget-pos';
@@ -2675,21 +2730,42 @@ class ContentCredits {
2675
2730
  async _start() {
2676
2731
  // 1. Consume any token that arrived in the URL (mobile redirect flow)
2677
2732
  consumeTokenFromUrl();
2678
- // 2. Hide premium content immediately (synchronous) before any async work.
2733
+ // 2. Wire config-level callbacks so developers don't need separate on() calls.
2734
+ if (this.config.onStateChange) {
2735
+ this.state.subscribe(this.config.onStateChange);
2736
+ }
2737
+ if (this.config.onReady) {
2738
+ this.emitter.on('ready', ({ state }) => this.config.onReady(state));
2739
+ }
2740
+ if (this.config.onPurchased) {
2741
+ this.emitter.on('article:purchased', (payload) => this.config.onPurchased(payload));
2742
+ }
2743
+ if (this.config.onUserLogin) {
2744
+ this.emitter.on('auth:login', ({ user }) => this.config.onUserLogin(user));
2745
+ }
2746
+ if (this.config.onUserLogout) {
2747
+ this.emitter.on('auth:logout', () => this.config.onUserLogout());
2748
+ }
2749
+ if (this.config.onError) {
2750
+ this.emitter.on('error', (payload) => this.config.onError(payload));
2751
+ }
2752
+ // 3. Hide premium content immediately (synchronous) before any async work.
2679
2753
  // This prevents the flash of full article content that would otherwise
2680
2754
  // appear during the token-refresh and access-check network round-trips.
2755
+ // Skipped in headless mode — the host app owns all DOM manipulation.
2681
2756
  const earlyGate = createGate({
2682
2757
  selector: this.config.contentSelector,
2683
2758
  teaserParagraphs: this.config.teaserParagraphs,
2684
2759
  });
2685
- earlyGate.hide();
2686
- // 3. If no access token in memory/session, attempt a silent refresh.
2760
+ if (!this.config.headless)
2761
+ earlyGate.hide();
2762
+ // 4. If no access token in memory/session, attempt a silent refresh.
2687
2763
  // This runs on every new browser session (after the browser was closed)
2688
2764
  // and silently re-authenticates the user using their stored refresh token.
2689
2765
  if (!tokenStorage.has()) {
2690
2766
  await tryRefreshSession(this.config.apiBaseUrl);
2691
2767
  }
2692
- // Pass the pre-created gate so createPaywall reuses the same instance
2768
+ // 5. Pass the pre-created gate so createPaywall reuses the same instance
2693
2769
  // (and its hiddenNodes list) rather than creating a second one.
2694
2770
  this.paywallModule = createPaywall(this.config, this.creditsApi, this.state, this.emitter, earlyGate);
2695
2771
  if (this.config.enableComments) {
@@ -2700,6 +2776,63 @@ class ContentCredits {
2700
2776
  this.emitter.emit('ready', { state: this.state.get() });
2701
2777
  }
2702
2778
  // ── Public API ────────────────────────────────────────────────────────────
2779
+ /**
2780
+ * Subscribe to state changes. The callback receives the full state snapshot
2781
+ * every time any field changes. Returns an unsubscribe function.
2782
+ *
2783
+ * Primarily useful in **headless mode** — lets you drive your own UI from
2784
+ * reactive state without polling `getState()`.
2785
+ *
2786
+ * @example
2787
+ * const unsubscribe = cc.subscribe((state) => {
2788
+ * if (state.hasAccess) showFullContent();
2789
+ * else showPaywall(state);
2790
+ * });
2791
+ */
2792
+ subscribe(fn) {
2793
+ return this.state.subscribe(fn);
2794
+ }
2795
+ /**
2796
+ * Trigger the login flow programmatically.
2797
+ *
2798
+ * - Desktop: opens a popup window to the Content Credits auth page.
2799
+ * - Mobile: performs a full-page redirect.
2800
+ * - Extension: delegates to the browser extension.
2801
+ *
2802
+ * Primarily useful in **headless mode** where you render your own "Login"
2803
+ * button and call this from its `onClick` handler.
2804
+ */
2805
+ async login() {
2806
+ var _a;
2807
+ await ((_a = this.paywallModule) === null || _a === void 0 ? void 0 : _a.login());
2808
+ }
2809
+ /**
2810
+ * Trigger the article purchase flow programmatically.
2811
+ *
2812
+ * Deducts the required credits from the user's balance and, on success,
2813
+ * updates `state.hasAccess` to `true` and emits `article:purchased`.
2814
+ *
2815
+ * If the user is not logged in, this automatically opens the login flow
2816
+ * first, then proceeds with the purchase.
2817
+ *
2818
+ * Primarily useful in **headless mode** where you render your own "Unlock"
2819
+ * button and call this from its `onClick` handler.
2820
+ */
2821
+ async purchase() {
2822
+ var _a;
2823
+ await ((_a = this.paywallModule) === null || _a === void 0 ? void 0 : _a.purchase());
2824
+ }
2825
+ /**
2826
+ * Open the Content Credits dashboard in a new tab so the user can top up
2827
+ * their credit balance.
2828
+ *
2829
+ * Primarily useful in **headless mode** when `state.creditBalance` is lower
2830
+ * than `state.requiredCredits`.
2831
+ */
2832
+ buyMoreCredits() {
2833
+ var _a;
2834
+ (_a = this.paywallModule) === null || _a === void 0 ? void 0 : _a.buyMoreCredits();
2835
+ }
2703
2836
  /** Subscribe to an SDK event. Returns an unsubscribe function. */
2704
2837
  on(event, handler) {
2705
2838
  return this.emitter.on(event, handler);
@@ -2741,7 +2874,7 @@ class ContentCredits {
2741
2874
  }
2742
2875
  /** SDK version string. */
2743
2876
  static get version() {
2744
- return "2.1.0";
2877
+ return "2.3.0";
2745
2878
  }
2746
2879
  }
2747
2880
  // ── Auto-init from script data attributes (CDN usage) ────────────────────────