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