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