@contentcredits/sdk 2.0.3 → 2.1.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/auth/popup.d.ts +12 -1
- package/dist/content-credits.cjs.js +143 -81
- package/dist/content-credits.cjs.js.map +1 -1
- package/dist/content-credits.esm.js +143 -81
- 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/paywall/index.d.ts +2 -1
- package/dist/ui/shadow.d.ts +8 -0
- package/package.json +1 -1
|
@@ -518,6 +518,20 @@ function createGate(options) {
|
|
|
518
518
|
}
|
|
519
519
|
contentEl.setAttribute(GATE_ATTR, 'true');
|
|
520
520
|
gated = true;
|
|
521
|
+
// Add a gradient fade at the bottom of the visible teaser so the cutoff
|
|
522
|
+
// looks intentional rather than abrupt.
|
|
523
|
+
if (!contentEl.querySelector('[data-cc-fade]')) {
|
|
524
|
+
const prevPos = contentEl.style.position;
|
|
525
|
+
if (!prevPos || prevPos === 'static')
|
|
526
|
+
contentEl.style.position = 'relative';
|
|
527
|
+
const fadeEl = document.createElement('div');
|
|
528
|
+
fadeEl.setAttribute('data-cc-fade', 'true');
|
|
529
|
+
fadeEl.style.cssText =
|
|
530
|
+
'position:absolute;bottom:0;left:0;width:100%;height:160px;' +
|
|
531
|
+
'background:linear-gradient(to bottom,transparent 0%,var(--cc-bg,#fff) 100%);' +
|
|
532
|
+
'pointer-events:none;z-index:1;';
|
|
533
|
+
contentEl.appendChild(fadeEl);
|
|
534
|
+
}
|
|
521
535
|
return true;
|
|
522
536
|
}
|
|
523
537
|
function reveal() {
|
|
@@ -529,6 +543,10 @@ function createGate(options) {
|
|
|
529
543
|
n.removeAttribute('data-cc-hidden');
|
|
530
544
|
}
|
|
531
545
|
});
|
|
546
|
+
// Remove gradient fade
|
|
547
|
+
const fadeEl = contentEl === null || contentEl === void 0 ? void 0 : contentEl.querySelector('[data-cc-fade]');
|
|
548
|
+
if (fadeEl)
|
|
549
|
+
fadeEl.remove();
|
|
532
550
|
hiddenNodes = [];
|
|
533
551
|
contentEl === null || contentEl === void 0 ? void 0 : contentEl.removeAttribute(GATE_ATTR);
|
|
534
552
|
gated = false;
|
|
@@ -561,6 +579,24 @@ function createShadowHost(id) {
|
|
|
561
579
|
host._ccShadow = root;
|
|
562
580
|
return { host, root };
|
|
563
581
|
}
|
|
582
|
+
/**
|
|
583
|
+
* Creates a shadow host inserted immediately after `anchorEl` in the DOM.
|
|
584
|
+
* Used for the inline paywall panel so it flows naturally below the content.
|
|
585
|
+
*/
|
|
586
|
+
function createInlineShadowHost(id, anchorEl) {
|
|
587
|
+
let host = document.getElementById(id);
|
|
588
|
+
if (!host) {
|
|
589
|
+
host = document.createElement('div');
|
|
590
|
+
host.id = id;
|
|
591
|
+
anchorEl.parentNode.insertBefore(host, anchorEl.nextSibling);
|
|
592
|
+
}
|
|
593
|
+
const existing = host._ccShadow;
|
|
594
|
+
if (existing)
|
|
595
|
+
return { host, root: existing };
|
|
596
|
+
const root = host.attachShadow({ mode: 'open' });
|
|
597
|
+
host._ccShadow = root;
|
|
598
|
+
return { host, root };
|
|
599
|
+
}
|
|
564
600
|
function removeShadowHost(id) {
|
|
565
601
|
const host = document.getElementById(id);
|
|
566
602
|
if (host)
|
|
@@ -583,56 +619,27 @@ function getPaywallStyles(primaryColor, fontFamily) {
|
|
|
583
619
|
return `
|
|
584
620
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
585
621
|
|
|
586
|
-
|
|
587
|
-
|
|
622
|
+
/* Inline paywall panel — sits below the teaser content in the page flow */
|
|
623
|
+
.cc-paywall-inline {
|
|
588
624
|
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 {
|
|
625
|
+
padding: 36px 24px 32px;
|
|
616
626
|
background: #fff;
|
|
617
627
|
border: 1px solid #e5e7eb;
|
|
618
|
-
border-
|
|
619
|
-
|
|
620
|
-
width: 100%;
|
|
621
|
-
max-width: 480px;
|
|
628
|
+
border-top: 3px solid ${primaryColor};
|
|
629
|
+
border-radius: 0 0 12px 12px;
|
|
622
630
|
text-align: center;
|
|
623
631
|
font-family: ${fontFamily};
|
|
624
|
-
box-
|
|
625
|
-
pointer-events: all;
|
|
632
|
+
box-sizing: border-box;
|
|
626
633
|
}
|
|
627
634
|
|
|
628
|
-
.cc-paywall-
|
|
635
|
+
.cc-paywall-inline h2 {
|
|
629
636
|
font-size: 20px;
|
|
630
637
|
font-weight: 700;
|
|
631
638
|
color: #111827;
|
|
632
639
|
margin-bottom: 8px;
|
|
633
640
|
}
|
|
634
641
|
|
|
635
|
-
.cc-paywall-
|
|
642
|
+
.cc-paywall-inline p {
|
|
636
643
|
font-size: 14px;
|
|
637
644
|
color: #6b7280;
|
|
638
645
|
margin-bottom: 24px;
|
|
@@ -1165,20 +1172,24 @@ function createPaywallRenderer(config) {
|
|
|
1165
1172
|
let root = null;
|
|
1166
1173
|
let overlay = null;
|
|
1167
1174
|
function init() {
|
|
1168
|
-
|
|
1175
|
+
// Insert the shadow host immediately after the content element so the
|
|
1176
|
+
// paywall panel flows inline below the teaser — no modal, no backdrop.
|
|
1177
|
+
const contentEl = document.querySelector(config.contentSelector);
|
|
1178
|
+
if (!contentEl)
|
|
1179
|
+
return;
|
|
1180
|
+
const { root: shadowRoot } = createInlineShadowHost(HOST_ID, contentEl);
|
|
1169
1181
|
root = shadowRoot;
|
|
1170
1182
|
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
1183
|
overlay = el('div');
|
|
1177
|
-
overlay.className = 'cc-paywall-
|
|
1178
|
-
|
|
1184
|
+
overlay.className = 'cc-paywall-inline';
|
|
1185
|
+
root.appendChild(overlay);
|
|
1179
1186
|
}
|
|
1180
1187
|
function render(state, callbacks, meta) {
|
|
1181
1188
|
var _a, _b, _c;
|
|
1189
|
+
// During the initial access check the content is already hidden by the
|
|
1190
|
+
// gate — no need to show a spinner in the paywall slot.
|
|
1191
|
+
if (state === 'checking')
|
|
1192
|
+
return;
|
|
1182
1193
|
if (!overlay)
|
|
1183
1194
|
init();
|
|
1184
1195
|
if (!overlay)
|
|
@@ -1187,9 +1198,6 @@ function createPaywallRenderer(config) {
|
|
|
1187
1198
|
while (overlay.firstChild)
|
|
1188
1199
|
overlay.removeChild(overlay.firstChild);
|
|
1189
1200
|
switch (state) {
|
|
1190
|
-
case 'checking':
|
|
1191
|
-
renderChecking(overlay);
|
|
1192
|
-
break;
|
|
1193
1201
|
case 'login':
|
|
1194
1202
|
renderLogin(overlay, callbacks);
|
|
1195
1203
|
break;
|
|
@@ -1207,20 +1215,6 @@ function createPaywallRenderer(config) {
|
|
|
1207
1215
|
break;
|
|
1208
1216
|
}
|
|
1209
1217
|
}
|
|
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
1218
|
function renderLogin(parent, cb) {
|
|
1225
1219
|
parent.appendChild(el('h2', 'This article requires a subscription'));
|
|
1226
1220
|
parent.appendChild(el('p', 'Log in with your Content Credits account to unlock this article.'));
|
|
@@ -1465,6 +1459,10 @@ function scrubTokenFromUrl() {
|
|
|
1465
1459
|
* Read and store a token that may have been placed in the current URL
|
|
1466
1460
|
* (e.g. after a mobile redirect back from the accounts site).
|
|
1467
1461
|
* Always scrubs the token from the URL after reading it.
|
|
1462
|
+
*
|
|
1463
|
+
* If we're running inside a popup window (window.opener exists), we notify
|
|
1464
|
+
* the opener via postMessage and close ourselves — this prevents the popup
|
|
1465
|
+
* from showing the full article page after a successful login redirect.
|
|
1468
1466
|
*/
|
|
1469
1467
|
function consumeTokenFromUrl() {
|
|
1470
1468
|
var _a, _b;
|
|
@@ -1479,33 +1477,90 @@ function consumeTokenFromUrl() {
|
|
|
1479
1477
|
}
|
|
1480
1478
|
if (token) {
|
|
1481
1479
|
tokenStorage.set(token);
|
|
1480
|
+
// If we're inside a popup (opened by openAuthPopup), notify the opener
|
|
1481
|
+
// and close instead of rendering the page. This fixes the bug where the
|
|
1482
|
+
// popup shows the blog article after the accounts redirect.
|
|
1483
|
+
if (window.opener && !window.opener.closed) {
|
|
1484
|
+
try {
|
|
1485
|
+
window.opener.postMessage({ type: 'cc_auth_callback', token, refreshToken: refreshToken !== null && refreshToken !== void 0 ? refreshToken : null }, window.location.origin);
|
|
1486
|
+
}
|
|
1487
|
+
catch (_c) {
|
|
1488
|
+
// opener is cross-origin or restricted — fall through, URL polling will handle it
|
|
1489
|
+
}
|
|
1490
|
+
// Brief delay so the opener can process the message before the popup closes
|
|
1491
|
+
setTimeout(() => { try {
|
|
1492
|
+
window.close();
|
|
1493
|
+
}
|
|
1494
|
+
catch ( /* ignore */_a) { /* ignore */ } }, 300);
|
|
1495
|
+
}
|
|
1482
1496
|
return token;
|
|
1483
1497
|
}
|
|
1484
1498
|
}
|
|
1485
|
-
catch (
|
|
1499
|
+
catch (_d) {
|
|
1486
1500
|
// ignore
|
|
1487
1501
|
}
|
|
1488
1502
|
return null;
|
|
1489
1503
|
}
|
|
1490
1504
|
/**
|
|
1491
|
-
* Open a centered auth popup and
|
|
1505
|
+
* Open a centered auth popup and wait for the token callback.
|
|
1506
|
+
*
|
|
1507
|
+
* Primary path: listens for a postMessage from the popup page (sent by
|
|
1508
|
+
* consumeTokenFromUrl when the accounts redirect lands on our origin).
|
|
1509
|
+
*
|
|
1510
|
+
* Fallback path: polls popup.location.href every 200 ms for cases where
|
|
1511
|
+
* postMessage isn't available (e.g. some mobile browsers, extensions).
|
|
1512
|
+
*
|
|
1492
1513
|
* Returns a promise that resolves with the token when login completes,
|
|
1493
1514
|
* or null if the popup is closed without completing login.
|
|
1494
1515
|
*/
|
|
1495
1516
|
function openAuthPopup(authUrl) {
|
|
1496
1517
|
return new Promise(resolve => {
|
|
1497
1518
|
let popup = null;
|
|
1519
|
+
let settled = false;
|
|
1520
|
+
function finish(token) {
|
|
1521
|
+
if (settled)
|
|
1522
|
+
return;
|
|
1523
|
+
settled = true;
|
|
1524
|
+
clearInterval(timer);
|
|
1525
|
+
window.removeEventListener('message', onMessage);
|
|
1526
|
+
resolve(token);
|
|
1527
|
+
}
|
|
1528
|
+
// ── Primary: postMessage from popup ───────────────────────────────────
|
|
1529
|
+
function onMessage(event) {
|
|
1530
|
+
var _a;
|
|
1531
|
+
// Only accept messages from our own origin
|
|
1532
|
+
if (event.origin !== window.location.origin)
|
|
1533
|
+
return;
|
|
1534
|
+
if (((_a = event.data) === null || _a === void 0 ? void 0 : _a.type) !== 'cc_auth_callback')
|
|
1535
|
+
return;
|
|
1536
|
+
const token = event.data.token;
|
|
1537
|
+
const refreshToken = event.data.refreshToken;
|
|
1538
|
+
if (!token)
|
|
1539
|
+
return;
|
|
1540
|
+
tokenStorage.set(token);
|
|
1541
|
+
if (refreshToken)
|
|
1542
|
+
refreshTokenStorage.set(refreshToken);
|
|
1543
|
+
try {
|
|
1544
|
+
popup === null || popup === void 0 ? void 0 : popup.close();
|
|
1545
|
+
}
|
|
1546
|
+
catch ( /* ignore */_b) { /* ignore */ }
|
|
1547
|
+
finish(token);
|
|
1548
|
+
}
|
|
1549
|
+
window.addEventListener('message', onMessage);
|
|
1498
1550
|
try {
|
|
1499
1551
|
popup = window.open(authUrl, POPUP_NAME, centeredSpecs());
|
|
1500
1552
|
}
|
|
1501
1553
|
catch (_a) {
|
|
1502
|
-
|
|
1554
|
+
window.removeEventListener('message', onMessage);
|
|
1555
|
+
resolve(null);
|
|
1556
|
+
return;
|
|
1503
1557
|
}
|
|
1504
|
-
// Popup blocked
|
|
1505
1558
|
if (!popup || popup.closed) {
|
|
1559
|
+
window.removeEventListener('message', onMessage);
|
|
1506
1560
|
resolve(null);
|
|
1507
1561
|
return;
|
|
1508
1562
|
}
|
|
1563
|
+
// ── Fallback: URL polling ─────────────────────────────────────────────
|
|
1509
1564
|
const POLL_MS = 200;
|
|
1510
1565
|
const MAX_WAIT_MS = 5 * 60 * 1000; // 5 minutes
|
|
1511
1566
|
let elapsed = 0;
|
|
@@ -1513,17 +1568,15 @@ function openAuthPopup(authUrl) {
|
|
|
1513
1568
|
var _a, _b;
|
|
1514
1569
|
elapsed += POLL_MS;
|
|
1515
1570
|
if (!popup || popup.closed) {
|
|
1516
|
-
|
|
1517
|
-
resolve(tokenStorage.get()); // may have been set just before close
|
|
1571
|
+
finish(tokenStorage.get());
|
|
1518
1572
|
return;
|
|
1519
1573
|
}
|
|
1520
1574
|
if (elapsed > MAX_WAIT_MS) {
|
|
1521
|
-
clearInterval(timer);
|
|
1522
1575
|
try {
|
|
1523
1576
|
popup.close();
|
|
1524
1577
|
}
|
|
1525
1578
|
catch ( /* ignore */_c) { /* ignore */ }
|
|
1526
|
-
|
|
1579
|
+
finish(null);
|
|
1527
1580
|
return;
|
|
1528
1581
|
}
|
|
1529
1582
|
try {
|
|
@@ -1535,15 +1588,13 @@ function openAuthPopup(authUrl) {
|
|
|
1535
1588
|
const refreshToken = (_b = params.get('refresh_token')) !== null && _b !== void 0 ? _b : params.get('cc_refresh_token');
|
|
1536
1589
|
if (token) {
|
|
1537
1590
|
tokenStorage.set(token);
|
|
1538
|
-
if (refreshToken)
|
|
1591
|
+
if (refreshToken)
|
|
1539
1592
|
refreshTokenStorage.set(refreshToken);
|
|
1540
|
-
}
|
|
1541
|
-
clearInterval(timer);
|
|
1542
1593
|
try {
|
|
1543
1594
|
popup.close();
|
|
1544
1595
|
}
|
|
1545
1596
|
catch ( /* ignore */_d) { /* ignore */ }
|
|
1546
|
-
|
|
1597
|
+
finish(token);
|
|
1547
1598
|
}
|
|
1548
1599
|
}
|
|
1549
1600
|
}
|
|
@@ -1554,8 +1605,10 @@ function openAuthPopup(authUrl) {
|
|
|
1554
1605
|
});
|
|
1555
1606
|
}
|
|
1556
1607
|
|
|
1557
|
-
function createPaywall(config, creditsApi, state, emitter) {
|
|
1558
|
-
|
|
1608
|
+
function createPaywall(config, creditsApi, state, emitter, existingGate) {
|
|
1609
|
+
// Accept a pre-created gate so the caller can call gate.hide() synchronously
|
|
1610
|
+
// before any async work, preventing a flash of the full article content.
|
|
1611
|
+
const gate = existingGate !== null && existingGate !== void 0 ? existingGate : createGate({
|
|
1559
1612
|
selector: config.contentSelector,
|
|
1560
1613
|
teaserParagraphs: config.teaserParagraphs,
|
|
1561
1614
|
});
|
|
@@ -2620,14 +2673,23 @@ class ContentCredits {
|
|
|
2620
2673
|
async _start() {
|
|
2621
2674
|
// 1. Consume any token that arrived in the URL (mobile redirect flow)
|
|
2622
2675
|
consumeTokenFromUrl();
|
|
2623
|
-
// 2.
|
|
2676
|
+
// 2. Hide premium content immediately (synchronous) before any async work.
|
|
2677
|
+
// This prevents the flash of full article content that would otherwise
|
|
2678
|
+
// appear during the token-refresh and access-check network round-trips.
|
|
2679
|
+
const earlyGate = createGate({
|
|
2680
|
+
selector: this.config.contentSelector,
|
|
2681
|
+
teaserParagraphs: this.config.teaserParagraphs,
|
|
2682
|
+
});
|
|
2683
|
+
earlyGate.hide();
|
|
2684
|
+
// 3. If no access token in memory/session, attempt a silent refresh.
|
|
2624
2685
|
// This runs on every new browser session (after the browser was closed)
|
|
2625
|
-
// and silently re-authenticates the user using their stored refresh token
|
|
2626
|
-
// — no popup, no visible delay, no paywall flash.
|
|
2686
|
+
// and silently re-authenticates the user using their stored refresh token.
|
|
2627
2687
|
if (!tokenStorage.has()) {
|
|
2628
2688
|
await tryRefreshSession(this.config.apiBaseUrl);
|
|
2629
2689
|
}
|
|
2630
|
-
|
|
2690
|
+
// Pass the pre-created gate so createPaywall reuses the same instance
|
|
2691
|
+
// (and its hiddenNodes list) rather than creating a second one.
|
|
2692
|
+
this.paywallModule = createPaywall(this.config, this.creditsApi, this.state, this.emitter, earlyGate);
|
|
2631
2693
|
if (this.config.enableComments) {
|
|
2632
2694
|
this.commentsModule = createComments(this.config, this.commentsApi, this.emitter);
|
|
2633
2695
|
this.commentsModule.init();
|
|
@@ -2677,7 +2739,7 @@ class ContentCredits {
|
|
|
2677
2739
|
}
|
|
2678
2740
|
/** SDK version string. */
|
|
2679
2741
|
static get version() {
|
|
2680
|
-
return "2.0
|
|
2742
|
+
return "2.1.0";
|
|
2681
2743
|
}
|
|
2682
2744
|
}
|
|
2683
2745
|
// ── Auto-init from script data attributes (CDN usage) ────────────────────────
|