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