@contentcredits/sdk 2.2.0 → 2.4.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.
- package/dist/api/client.d.ts +7 -7
- package/dist/api/comments.d.ts +3 -2
- package/dist/api/credits.d.ts +3 -2
- package/dist/comments/index.d.ts +7 -6
- package/dist/comments/panel.d.ts +6 -5
- package/dist/content-credits.cjs.js +110 -30
- package/dist/content-credits.cjs.js.map +1 -1
- package/dist/content-credits.d.ts +65 -0
- package/dist/content-credits.esm.js +110 -30
- package/dist/content-credits.esm.js.map +1 -1
- package/dist/content-credits.umd.min.js +1 -1
- package/dist/content-credits.umd.min.js.map +1 -1
- package/dist/core/events.d.ts +7 -7
- package/dist/core/state.d.ts +8 -8
- package/dist/extension/bridge.d.ts +11 -11
- package/dist/index.d.ts +7 -0
- package/dist/paywall/index.d.ts +9 -8
- package/dist/paywall/renderer.d.ts +10 -9
- package/dist/types/index.d.ts +80 -1
- package/package.json +6 -2
|
@@ -26,6 +26,15 @@ function resolveConfig(raw) {
|
|
|
26
26
|
accountsUrl: "https://accounts.contentcredits.com",
|
|
27
27
|
paywallTemplate: raw.paywallTemplate,
|
|
28
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,
|
|
29
38
|
theme: {
|
|
30
39
|
primaryColor: (_j = (_h = raw.theme) === null || _h === void 0 ? void 0 : _h.primaryColor) !== null && _j !== void 0 ? _j : '#44C678',
|
|
31
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",
|
|
@@ -175,10 +184,12 @@ const REFRESH_KEY = 'cc_rt';
|
|
|
175
184
|
* └─ Layer 3: localStorage — survives browser close; used to silently
|
|
176
185
|
* re-authenticate on the next visit without showing a popup
|
|
177
186
|
*
|
|
178
|
-
* We intentionally never write to document.cookie
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
187
|
+
* We intentionally never write to document.cookie — both localStorage and
|
|
188
|
+
* non-HttpOnly cookies are equally XSS-accessible. The truly safe option
|
|
189
|
+
* (HttpOnly server-set cookie) requires cross-site cookie support which
|
|
190
|
+
* browsers are phasing out for third-party embeds. localStorage is
|
|
191
|
+
* first-party (publisher-domain scoped), never blocked, and the risk is
|
|
192
|
+
* mitigated by short-lived access tokens and server-side refresh token rotation.
|
|
182
193
|
*/
|
|
183
194
|
let memoryToken = null;
|
|
184
195
|
const tokenStorage = {
|
|
@@ -1221,7 +1232,7 @@ function createPaywallRenderer(config) {
|
|
|
1221
1232
|
parent.appendChild(el('p', 'Log in with your Content Credits account to unlock this article.'));
|
|
1222
1233
|
const btn = el('button', 'Login & Buy with Content Credits');
|
|
1223
1234
|
btn.className = 'cc-btn cc-btn-primary';
|
|
1224
|
-
btn.addEventListener('click', cb.onLogin);
|
|
1235
|
+
btn.addEventListener('click', () => { void cb.onLogin(); });
|
|
1225
1236
|
parent.appendChild(btn);
|
|
1226
1237
|
parent.appendChild(poweredBy());
|
|
1227
1238
|
}
|
|
@@ -1235,7 +1246,7 @@ function createPaywallRenderer(config) {
|
|
|
1235
1246
|
parent.appendChild(el('p', 'Use your Content Credits balance to instantly access this premium article.'));
|
|
1236
1247
|
const btn = el('button', credits !== null ? `Buy for ${credits} Credit${credits !== 1 ? 's' : ''}` : 'Buy with Content Credits');
|
|
1237
1248
|
btn.className = 'cc-btn cc-btn-primary';
|
|
1238
|
-
btn.addEventListener('click', cb.onPurchase);
|
|
1249
|
+
btn.addEventListener('click', () => { void cb.onPurchase(); });
|
|
1239
1250
|
parent.appendChild(btn);
|
|
1240
1251
|
parent.appendChild(poweredBy());
|
|
1241
1252
|
}
|
|
@@ -1247,7 +1258,7 @@ function createPaywallRenderer(config) {
|
|
|
1247
1258
|
parent.appendChild(el('p', detail));
|
|
1248
1259
|
const btn = el('button', 'Buy More Credits');
|
|
1249
1260
|
btn.className = 'cc-btn cc-btn-primary';
|
|
1250
|
-
btn.addEventListener('click', cb.onBuyMoreCredits);
|
|
1261
|
+
btn.addEventListener('click', () => cb.onBuyMoreCredits());
|
|
1251
1262
|
parent.appendChild(btn);
|
|
1252
1263
|
parent.appendChild(poweredBy());
|
|
1253
1264
|
}
|
|
@@ -1421,6 +1432,11 @@ function createExtensionBridge() {
|
|
|
1421
1432
|
};
|
|
1422
1433
|
}
|
|
1423
1434
|
|
|
1435
|
+
function isAuthCallback(data) {
|
|
1436
|
+
return (typeof data === 'object' &&
|
|
1437
|
+
data !== null &&
|
|
1438
|
+
data.type === 'cc_auth_callback');
|
|
1439
|
+
}
|
|
1424
1440
|
const POPUP_NAME = 'ccAuthPopup';
|
|
1425
1441
|
const POPUP_SPECS = 'scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,width=600,height=650';
|
|
1426
1442
|
function centeredSpecs() {
|
|
@@ -1481,9 +1497,10 @@ function consumeTokenFromUrl() {
|
|
|
1481
1497
|
// If we're inside a popup (opened by openAuthPopup), notify the opener
|
|
1482
1498
|
// and close instead of rendering the page. This fixes the bug where the
|
|
1483
1499
|
// popup shows the blog article after the accounts redirect.
|
|
1484
|
-
|
|
1500
|
+
const opener = window.opener;
|
|
1501
|
+
if (opener && !opener.closed) {
|
|
1485
1502
|
try {
|
|
1486
|
-
|
|
1503
|
+
opener.postMessage({ type: 'cc_auth_callback', token, refreshToken: refreshToken !== null && refreshToken !== void 0 ? refreshToken : null }, window.location.origin);
|
|
1487
1504
|
}
|
|
1488
1505
|
catch (_c) {
|
|
1489
1506
|
// opener is cross-origin or restricted — fall through, URL polling will handle it
|
|
@@ -1528,11 +1545,10 @@ function openAuthPopup(authUrl) {
|
|
|
1528
1545
|
}
|
|
1529
1546
|
// ── Primary: postMessage from popup ───────────────────────────────────
|
|
1530
1547
|
function onMessage(event) {
|
|
1531
|
-
var _a;
|
|
1532
1548
|
// Only accept messages from our own origin
|
|
1533
1549
|
if (event.origin !== window.location.origin)
|
|
1534
1550
|
return;
|
|
1535
|
-
if ((
|
|
1551
|
+
if (!isAuthCallback(event.data))
|
|
1536
1552
|
return;
|
|
1537
1553
|
const token = event.data.token;
|
|
1538
1554
|
const refreshToken = event.data.refreshToken;
|
|
@@ -1544,7 +1560,7 @@ function openAuthPopup(authUrl) {
|
|
|
1544
1560
|
try {
|
|
1545
1561
|
popup === null || popup === void 0 ? void 0 : popup.close();
|
|
1546
1562
|
}
|
|
1547
|
-
catch ( /* ignore */
|
|
1563
|
+
catch ( /* ignore */_a) { /* ignore */ }
|
|
1548
1564
|
finish(token);
|
|
1549
1565
|
}
|
|
1550
1566
|
window.addEventListener('message', onMessage);
|
|
@@ -1659,7 +1675,7 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
|
|
|
1659
1675
|
}
|
|
1660
1676
|
// ── Purchase ──────────────────────────────────────────────────────────────
|
|
1661
1677
|
async function doPurchase() {
|
|
1662
|
-
var _a, _b, _c;
|
|
1678
|
+
var _a, _b, _c, _d, _e;
|
|
1663
1679
|
if (!tokenStorage.has()) {
|
|
1664
1680
|
await doLogin();
|
|
1665
1681
|
return;
|
|
@@ -1703,14 +1719,18 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
|
|
|
1703
1719
|
creditBalance: state.get().creditBalance,
|
|
1704
1720
|
});
|
|
1705
1721
|
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
});
|
|
1722
|
+
const required = (_b = state.get().requiredCredits) !== null && _b !== void 0 ? _b : 0;
|
|
1723
|
+
const available = (_c = state.get().creditBalance) !== null && _c !== void 0 ? _c : 0;
|
|
1724
|
+
(_d = config.onInsufficientCredits) === null || _d === void 0 ? void 0 : _d.call(config, { required, available });
|
|
1725
|
+
emitter.emit('credits:insufficient', { required, available });
|
|
1710
1726
|
}
|
|
1711
1727
|
else {
|
|
1712
1728
|
if (!config.headless)
|
|
1713
1729
|
renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
1730
|
+
(_e = config.onPurchaseRequired) === null || _e === void 0 ? void 0 : _e.call(config, {
|
|
1731
|
+
requiredCredits: state.get().requiredCredits,
|
|
1732
|
+
creditBalance: state.get().creditBalance,
|
|
1733
|
+
});
|
|
1714
1734
|
emitter.emit('error', { message: 'Purchase failed', error: err });
|
|
1715
1735
|
}
|
|
1716
1736
|
}
|
|
@@ -1720,6 +1740,7 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
|
|
|
1720
1740
|
}
|
|
1721
1741
|
// ── Access Check ──────────────────────────────────────────────────────────
|
|
1722
1742
|
async function checkAccess() {
|
|
1743
|
+
var _a, _b, _c;
|
|
1723
1744
|
state.set({ isLoading: true });
|
|
1724
1745
|
if (!config.headless)
|
|
1725
1746
|
renderer.render('checking', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
@@ -1733,6 +1754,7 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
|
|
|
1733
1754
|
gate.hide();
|
|
1734
1755
|
renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
1735
1756
|
}
|
|
1757
|
+
(_a = config.onLoginRequired) === null || _a === void 0 ? void 0 : _a.call(config);
|
|
1736
1758
|
emitter.emit('paywall:shown', {});
|
|
1737
1759
|
return;
|
|
1738
1760
|
}
|
|
@@ -1756,6 +1778,10 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
|
|
|
1756
1778
|
gate.hide();
|
|
1757
1779
|
renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
1758
1780
|
}
|
|
1781
|
+
(_b = config.onPurchaseRequired) === null || _b === void 0 ? void 0 : _b.call(config, {
|
|
1782
|
+
requiredCredits: state.get().requiredCredits,
|
|
1783
|
+
creditBalance: state.get().creditBalance,
|
|
1784
|
+
});
|
|
1759
1785
|
emitter.emit('paywall:shown', {});
|
|
1760
1786
|
}
|
|
1761
1787
|
}
|
|
@@ -1765,6 +1791,7 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
|
|
|
1765
1791
|
gate.hide();
|
|
1766
1792
|
renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
1767
1793
|
}
|
|
1794
|
+
(_c = config.onLoginRequired) === null || _c === void 0 ? void 0 : _c.call(config);
|
|
1768
1795
|
if (!(err instanceof ApiError && err.status === 401)) {
|
|
1769
1796
|
emitter.emit('error', { message: 'Access check failed', error: err });
|
|
1770
1797
|
}
|
|
@@ -1783,7 +1810,7 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
|
|
|
1783
1810
|
if (extensionAvailable) {
|
|
1784
1811
|
bridge.attach();
|
|
1785
1812
|
bridge.onAuthorizationResponse(data => {
|
|
1786
|
-
var _a, _b, _c;
|
|
1813
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1787
1814
|
state.set({
|
|
1788
1815
|
isLoggedIn: data.isAuthenticated,
|
|
1789
1816
|
hasAccess: data.doesHaveAccess,
|
|
@@ -1797,10 +1824,11 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
|
|
|
1797
1824
|
gate.hide();
|
|
1798
1825
|
renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
1799
1826
|
}
|
|
1827
|
+
(_c = config.onLoginRequired) === null || _c === void 0 ? void 0 : _c.call(config);
|
|
1800
1828
|
emitter.emit('paywall:shown', {});
|
|
1801
1829
|
}
|
|
1802
1830
|
else if (data.doesHaveAccess) {
|
|
1803
|
-
handleAccessGranted(0, (
|
|
1831
|
+
handleAccessGranted(0, (_d = data.creditBalance) !== null && _d !== void 0 ? _d : 0);
|
|
1804
1832
|
}
|
|
1805
1833
|
else {
|
|
1806
1834
|
if (!config.headless) {
|
|
@@ -1810,6 +1838,10 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
|
|
|
1810
1838
|
creditBalance: data.creditBalance,
|
|
1811
1839
|
});
|
|
1812
1840
|
}
|
|
1841
|
+
(_e = config.onPurchaseRequired) === null || _e === void 0 ? void 0 : _e.call(config, {
|
|
1842
|
+
requiredCredits: (_f = data.requiredCredits) !== null && _f !== void 0 ? _f : null,
|
|
1843
|
+
creditBalance: (_g = data.creditBalance) !== null && _g !== void 0 ? _g : null,
|
|
1844
|
+
});
|
|
1813
1845
|
emitter.emit('paywall:shown', {});
|
|
1814
1846
|
}
|
|
1815
1847
|
});
|
|
@@ -2595,15 +2627,18 @@ function createCommentPanel(config, commentsApi, emitter, onClose) {
|
|
|
2595
2627
|
refreshUser();
|
|
2596
2628
|
refreshUser();
|
|
2597
2629
|
// Build panel if not already present
|
|
2598
|
-
let
|
|
2599
|
-
if (!
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
r.appendChild(
|
|
2630
|
+
let backdropEl = r.getElementById('cc-panel-backdrop');
|
|
2631
|
+
if (!backdropEl) {
|
|
2632
|
+
const newBackdrop = el('div');
|
|
2633
|
+
newBackdrop.className = 'cc-panel-backdrop';
|
|
2634
|
+
newBackdrop.id = 'cc-panel-backdrop';
|
|
2635
|
+
newBackdrop.addEventListener('click', closePanel);
|
|
2636
|
+
r.appendChild(newBackdrop);
|
|
2605
2637
|
r.appendChild(buildPanel());
|
|
2638
|
+
backdropEl = newBackdrop;
|
|
2606
2639
|
}
|
|
2640
|
+
// Capture in a const so the rAF callback has a non-nullable reference
|
|
2641
|
+
const backdrop = backdropEl;
|
|
2607
2642
|
// Animate open
|
|
2608
2643
|
requestAnimationFrame(() => {
|
|
2609
2644
|
var _a;
|
|
@@ -2703,7 +2738,26 @@ class ContentCredits {
|
|
|
2703
2738
|
async _start() {
|
|
2704
2739
|
// 1. Consume any token that arrived in the URL (mobile redirect flow)
|
|
2705
2740
|
consumeTokenFromUrl();
|
|
2706
|
-
// 2.
|
|
2741
|
+
// 2. Wire config-level callbacks so developers don't need separate on() calls.
|
|
2742
|
+
if (this.config.onStateChange) {
|
|
2743
|
+
this.state.subscribe(this.config.onStateChange);
|
|
2744
|
+
}
|
|
2745
|
+
if (this.config.onReady) {
|
|
2746
|
+
this.emitter.on('ready', ({ state }) => this.config.onReady(state));
|
|
2747
|
+
}
|
|
2748
|
+
if (this.config.onPurchased) {
|
|
2749
|
+
this.emitter.on('article:purchased', (payload) => this.config.onPurchased(payload));
|
|
2750
|
+
}
|
|
2751
|
+
if (this.config.onUserLogin) {
|
|
2752
|
+
this.emitter.on('auth:login', ({ user }) => this.config.onUserLogin(user));
|
|
2753
|
+
}
|
|
2754
|
+
if (this.config.onUserLogout) {
|
|
2755
|
+
this.emitter.on('auth:logout', () => this.config.onUserLogout());
|
|
2756
|
+
}
|
|
2757
|
+
if (this.config.onError) {
|
|
2758
|
+
this.emitter.on('error', (payload) => this.config.onError(payload));
|
|
2759
|
+
}
|
|
2760
|
+
// 3. Hide premium content immediately (synchronous) before any async work.
|
|
2707
2761
|
// This prevents the flash of full article content that would otherwise
|
|
2708
2762
|
// appear during the token-refresh and access-check network round-trips.
|
|
2709
2763
|
// Skipped in headless mode — the host app owns all DOM manipulation.
|
|
@@ -2713,13 +2767,13 @@ class ContentCredits {
|
|
|
2713
2767
|
});
|
|
2714
2768
|
if (!this.config.headless)
|
|
2715
2769
|
earlyGate.hide();
|
|
2716
|
-
//
|
|
2770
|
+
// 4. If no access token in memory/session, attempt a silent refresh.
|
|
2717
2771
|
// This runs on every new browser session (after the browser was closed)
|
|
2718
2772
|
// and silently re-authenticates the user using their stored refresh token.
|
|
2719
2773
|
if (!tokenStorage.has()) {
|
|
2720
2774
|
await tryRefreshSession(this.config.apiBaseUrl);
|
|
2721
2775
|
}
|
|
2722
|
-
// Pass the pre-created gate so createPaywall reuses the same instance
|
|
2776
|
+
// 5. Pass the pre-created gate so createPaywall reuses the same instance
|
|
2723
2777
|
// (and its hiddenNodes list) rather than creating a second one.
|
|
2724
2778
|
this.paywallModule = createPaywall(this.config, this.creditsApi, this.state, this.emitter, earlyGate);
|
|
2725
2779
|
if (this.config.enableComments) {
|
|
@@ -2818,6 +2872,32 @@ class ContentCredits {
|
|
|
2818
2872
|
isLoggedIn() {
|
|
2819
2873
|
return tokenStorage.has();
|
|
2820
2874
|
}
|
|
2875
|
+
/**
|
|
2876
|
+
* Log the current user out.
|
|
2877
|
+
*
|
|
2878
|
+
* Revokes the refresh token on the server (best-effort), clears all local
|
|
2879
|
+
* auth state, resets SDK state, and emits `auth:logout`.
|
|
2880
|
+
*/
|
|
2881
|
+
async logout() {
|
|
2882
|
+
const rt = refreshTokenStorage.get();
|
|
2883
|
+
if (rt) {
|
|
2884
|
+
try {
|
|
2885
|
+
await fetch(`${this.config.apiBaseUrl}/auth/log-out`, {
|
|
2886
|
+
method: 'POST',
|
|
2887
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2888
|
+
body: JSON.stringify({ refreshToken: rt }),
|
|
2889
|
+
credentials: 'omit',
|
|
2890
|
+
});
|
|
2891
|
+
}
|
|
2892
|
+
catch (_a) {
|
|
2893
|
+
// Network error — proceed with local cleanup regardless
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
tokenStorage.clear();
|
|
2897
|
+
refreshTokenStorage.clear();
|
|
2898
|
+
this.state.reset();
|
|
2899
|
+
this.emitter.emit('auth:logout', {});
|
|
2900
|
+
}
|
|
2821
2901
|
/** Tear down the SDK — removes all UI, event listeners, and stored state. */
|
|
2822
2902
|
destroy() {
|
|
2823
2903
|
var _a, _b;
|
|
@@ -2828,7 +2908,7 @@ class ContentCredits {
|
|
|
2828
2908
|
}
|
|
2829
2909
|
/** SDK version string. */
|
|
2830
2910
|
static get version() {
|
|
2831
|
-
return "2.
|
|
2911
|
+
return "2.4.0";
|
|
2832
2912
|
}
|
|
2833
2913
|
}
|
|
2834
2914
|
// ── Auto-init from script data attributes (CDN usage) ────────────────────────
|