@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.
- package/dist/content-credits.cjs.js +195 -101
- package/dist/content-credits.cjs.js.map +1 -1
- package/dist/content-credits.d.ts +60 -0
- package/dist/content-credits.esm.js +195 -101
- 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/index.d.ts +46 -0
- package/dist/paywall/index.d.ts +5 -1
- package/dist/types/index.d.ts +14 -0
- package/dist/ui/shadow.d.ts +8 -0
- package/package.json +1 -1
|
@@ -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 (
|
|
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: (
|
|
32
|
-
fontFamily: (
|
|
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
|
-
|
|
589
|
-
|
|
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-
|
|
621
|
-
|
|
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-
|
|
627
|
-
pointer-events: all;
|
|
635
|
+
box-sizing: border-box;
|
|
628
636
|
}
|
|
629
637
|
|
|
630
|
-
.cc-paywall-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
1180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1633
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1728
|
-
|
|
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
|
-
|
|
1749
|
-
|
|
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
|
-
|
|
1756
|
-
|
|
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
|
-
|
|
1786
|
-
|
|
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
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
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
|
-
|
|
1818
|
-
|
|
1834
|
+
if (!config.headless) {
|
|
1835
|
+
renderer.destroy();
|
|
1836
|
+
gate.reveal();
|
|
1837
|
+
}
|
|
1819
1838
|
}
|
|
1820
|
-
return {
|
|
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.
|
|
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
|
-
|
|
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
|
|
2833
|
+
return "2.2.0";
|
|
2740
2834
|
}
|
|
2741
2835
|
}
|
|
2742
2836
|
// ── Auto-init from script data attributes (CDN usage) ────────────────────────
|