@contentcredits/sdk 2.11.0 → 2.13.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/README.md +138 -8
- package/dist/content-credits.cjs.js +341 -84
- package/dist/content-credits.cjs.js.map +1 -1
- package/dist/content-credits.d.ts +60 -2
- package/dist/content-credits.esm.js +341 -84
- 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/gate.d.ts +5 -0
- package/dist/types/index.d.ts +64 -4
- package/package.json +1 -1
|
@@ -13,7 +13,7 @@ function normalizeArticleUrl(articleUrl) {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
function resolveConfig(raw) {
|
|
16
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
16
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
17
17
|
if (!raw.apiKey || typeof raw.apiKey !== 'string' || raw.apiKey.trim() === '') {
|
|
18
18
|
throw new Error('[ContentCredits] apiKey is required. Get yours from the Content Credits admin panel.');
|
|
19
19
|
}
|
|
@@ -22,7 +22,7 @@ function resolveConfig(raw) {
|
|
|
22
22
|
try {
|
|
23
23
|
hostName = new URL(articleUrl).hostname;
|
|
24
24
|
}
|
|
25
|
-
catch (
|
|
25
|
+
catch (_o) {
|
|
26
26
|
throw new Error(`[ContentCredits] Invalid articleUrl: "${articleUrl}"`);
|
|
27
27
|
}
|
|
28
28
|
return {
|
|
@@ -36,9 +36,11 @@ function resolveConfig(raw) {
|
|
|
36
36
|
extensionId: (_e = raw.extensionId) !== null && _e !== void 0 ? _e : "ljehdpabbhgccmanhjdfacjnaigpgcml",
|
|
37
37
|
debug: (_f = raw.debug) !== null && _f !== void 0 ? _f : false,
|
|
38
38
|
headless: (_g = raw.headless) !== null && _g !== void 0 ? _g : false,
|
|
39
|
+
paywallMode: (_h = raw.paywallMode) !== null && _h !== void 0 ? _h : 'overlay',
|
|
40
|
+
paywallTopSlot: raw.paywallTopSlot,
|
|
41
|
+
reactDOM: raw.reactDOM,
|
|
39
42
|
apiBaseUrl: "https://api.contentcredits.com",
|
|
40
43
|
accountsUrl: "https://accounts.contentcredits.com",
|
|
41
|
-
paywallTemplate: raw.paywallTemplate,
|
|
42
44
|
onAccessGranted: raw.onAccessGranted,
|
|
43
45
|
onStateChange: raw.onStateChange,
|
|
44
46
|
onReady: raw.onReady,
|
|
@@ -50,8 +52,8 @@ function resolveConfig(raw) {
|
|
|
50
52
|
onUserLogout: raw.onUserLogout,
|
|
51
53
|
onError: raw.onError,
|
|
52
54
|
theme: {
|
|
53
|
-
primaryColor: (
|
|
54
|
-
fontFamily: (
|
|
55
|
+
primaryColor: (_k = (_j = raw.theme) === null || _j === void 0 ? void 0 : _j.primaryColor) !== null && _k !== void 0 ? _k : '#44C678',
|
|
56
|
+
fontFamily: (_m = (_l = raw.theme) === null || _l === void 0 ? void 0 : _l.fontFamily) !== null && _m !== void 0 ? _m : "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
55
57
|
},
|
|
56
58
|
};
|
|
57
59
|
}
|
|
@@ -544,9 +546,9 @@ function createGate(options) {
|
|
|
544
546
|
}
|
|
545
547
|
contentEl.setAttribute(GATE_ATTR, 'true');
|
|
546
548
|
gated = true;
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
if (!contentEl.querySelector('[data-cc-fade]')) {
|
|
549
|
+
// In overlay mode the gradient is part of the paywall panel itself.
|
|
550
|
+
// In inline mode inject a fade element at the bottom of the teaser.
|
|
551
|
+
if (options.paywallMode === 'inline' && !contentEl.querySelector('[data-cc-fade]')) {
|
|
550
552
|
const prevPos = contentEl.style.position;
|
|
551
553
|
if (!prevPos || prevPos === 'static')
|
|
552
554
|
contentEl.style.position = 'relative';
|
|
@@ -645,7 +647,7 @@ function getPaywallStyles(primaryColor, fontFamily) {
|
|
|
645
647
|
return `
|
|
646
648
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
647
649
|
|
|
648
|
-
/* Inline paywall panel — sits below
|
|
650
|
+
/* ── Inline paywall panel — sits below teaser in the page flow ── */
|
|
649
651
|
.cc-paywall-inline {
|
|
650
652
|
width: 100%;
|
|
651
653
|
padding: 36px 24px 32px;
|
|
@@ -672,6 +674,87 @@ function getPaywallStyles(primaryColor, fontFamily) {
|
|
|
672
674
|
line-height: 1.6;
|
|
673
675
|
}
|
|
674
676
|
|
|
677
|
+
/* ── Overlay paywall panel — full-width white panel below gated content ── */
|
|
678
|
+
.cc-paywall-overlay {
|
|
679
|
+
/* Break out of any max-width container to span the full viewport */
|
|
680
|
+
width: 100vw;
|
|
681
|
+
margin-left: calc(50% - 50vw);
|
|
682
|
+
background: #fff;
|
|
683
|
+
font-family: ${fontFamily};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/* Gradient that fades the article into the white panel */
|
|
687
|
+
.cc-paywall-overlay-gradient {
|
|
688
|
+
width: 100%;
|
|
689
|
+
height: 120px;
|
|
690
|
+
background: linear-gradient(to bottom, transparent 0%, #fff 100%);
|
|
691
|
+
margin-top: -120px;
|
|
692
|
+
pointer-events: none;
|
|
693
|
+
position: relative;
|
|
694
|
+
z-index: 1;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/* Top slot — client-supplied content */
|
|
698
|
+
.cc-paywall-overlay-slot {
|
|
699
|
+
padding: 32px 24px 0;
|
|
700
|
+
display: flex;
|
|
701
|
+
flex-direction: column;
|
|
702
|
+
align-items: center;
|
|
703
|
+
gap: 12px;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/* Our SDK's unlock section below the slot */
|
|
707
|
+
.cc-paywall-overlay-body {
|
|
708
|
+
padding: 20px 24px 32px;
|
|
709
|
+
display: flex;
|
|
710
|
+
flex-direction: column;
|
|
711
|
+
align-items: center;
|
|
712
|
+
gap: 0;
|
|
713
|
+
text-align: center;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/* Slot item — heading */
|
|
717
|
+
.cc-slot-heading {
|
|
718
|
+
font-size: 22px;
|
|
719
|
+
font-weight: 700;
|
|
720
|
+
color: #111827;
|
|
721
|
+
text-align: center;
|
|
722
|
+
line-height: 1.3;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/* Slot item — subheading */
|
|
726
|
+
.cc-slot-subheading {
|
|
727
|
+
font-size: 16px;
|
|
728
|
+
font-weight: 600;
|
|
729
|
+
color: #374151;
|
|
730
|
+
text-align: center;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/* Slot item — body text */
|
|
734
|
+
.cc-slot-text {
|
|
735
|
+
font-size: 14px;
|
|
736
|
+
color: #6b7280;
|
|
737
|
+
text-align: center;
|
|
738
|
+
line-height: 1.6;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/* Slot item — divider with optional label */
|
|
742
|
+
.cc-slot-divider {
|
|
743
|
+
display: flex;
|
|
744
|
+
align-items: center;
|
|
745
|
+
gap: 12px;
|
|
746
|
+
width: 100%;
|
|
747
|
+
max-width: 320px;
|
|
748
|
+
font-size: 13px;
|
|
749
|
+
color: #9ca3af;
|
|
750
|
+
}
|
|
751
|
+
.cc-slot-divider::before,
|
|
752
|
+
.cc-slot-divider::after {
|
|
753
|
+
content: '';
|
|
754
|
+
flex: 1;
|
|
755
|
+
border-top: 1px solid #e5e7eb;
|
|
756
|
+
}
|
|
757
|
+
|
|
675
758
|
.cc-btn {
|
|
676
759
|
display: inline-flex;
|
|
677
760
|
align-items: center;
|
|
@@ -1196,45 +1279,84 @@ function sanitizeUrl(url) {
|
|
|
1196
1279
|
const HOST_ID = 'cc-paywall-host';
|
|
1197
1280
|
function createPaywallRenderer(config) {
|
|
1198
1281
|
let root = null;
|
|
1199
|
-
|
|
1282
|
+
// In inline mode: the single panel div. In overlay mode: the body section only.
|
|
1283
|
+
let body = null;
|
|
1284
|
+
// Tracks a mounted React 18 root so we can unmount it on destroy.
|
|
1285
|
+
let reactRoot = null;
|
|
1200
1286
|
function init() {
|
|
1201
|
-
// Insert the shadow host immediately after the content element so the
|
|
1202
|
-
// paywall panel flows inline below the teaser — no modal, no backdrop.
|
|
1203
1287
|
const contentEl = document.querySelector(config.contentSelector);
|
|
1204
1288
|
if (!contentEl)
|
|
1205
1289
|
return;
|
|
1206
1290
|
const { root: shadowRoot } = createInlineShadowHost(HOST_ID, contentEl);
|
|
1207
1291
|
root = shadowRoot;
|
|
1208
1292
|
injectStyles(root, getPaywallStyles(config.theme.primaryColor, config.theme.fontFamily));
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1293
|
+
if (config.paywallMode === 'overlay') {
|
|
1294
|
+
initOverlay(root, contentEl);
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
initInline(root);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function initInline(shadowRoot) {
|
|
1301
|
+
body = el('div');
|
|
1302
|
+
body.className = 'cc-paywall-inline';
|
|
1303
|
+
shadowRoot.appendChild(body);
|
|
1304
|
+
}
|
|
1305
|
+
function initOverlay(shadowRoot, contentEl) {
|
|
1306
|
+
var _a, _b;
|
|
1307
|
+
const panel = el('div');
|
|
1308
|
+
panel.className = 'cc-paywall-overlay';
|
|
1309
|
+
// Gradient that visually fades the article into the white panel.
|
|
1310
|
+
// We read the computed background of the content element's parent so the
|
|
1311
|
+
// gradient blends correctly on sites with non-white backgrounds.
|
|
1312
|
+
const gradient = el('div');
|
|
1313
|
+
gradient.className = 'cc-paywall-overlay-gradient';
|
|
1314
|
+
const parentBg = getComputedStyle((_a = contentEl.parentElement) !== null && _a !== void 0 ? _a : contentEl).backgroundColor;
|
|
1315
|
+
if (parentBg && parentBg !== 'rgba(0, 0, 0, 0)' && parentBg !== 'transparent') {
|
|
1316
|
+
gradient.style.background = `linear-gradient(to bottom, transparent 0%, ${parentBg} 100%)`;
|
|
1317
|
+
}
|
|
1318
|
+
panel.appendChild(gradient);
|
|
1319
|
+
// Top slot — client content
|
|
1320
|
+
const slot = el('div');
|
|
1321
|
+
slot.className = 'cc-paywall-overlay-slot';
|
|
1322
|
+
reactRoot = (_b = mountTopSlot(slot, config.paywallTopSlot, config.reactDOM)) !== null && _b !== void 0 ? _b : null;
|
|
1323
|
+
panel.appendChild(slot);
|
|
1324
|
+
// Only render the "or" divider between slot and body when the slot has content
|
|
1325
|
+
if (config.paywallTopSlot) {
|
|
1326
|
+
const divider = el('div');
|
|
1327
|
+
divider.className = 'cc-slot-divider';
|
|
1328
|
+
divider.style.cssText = 'margin: 4px auto 0; padding: 0 24px;';
|
|
1329
|
+
divider.textContent = 'or';
|
|
1330
|
+
panel.appendChild(divider);
|
|
1331
|
+
}
|
|
1332
|
+
// Our SDK's unlock body
|
|
1333
|
+
body = el('div');
|
|
1334
|
+
body.className = 'cc-paywall-overlay-body';
|
|
1335
|
+
panel.appendChild(body);
|
|
1336
|
+
shadowRoot.appendChild(panel);
|
|
1212
1337
|
}
|
|
1213
1338
|
function render(state, callbacks, meta) {
|
|
1214
1339
|
var _a, _b, _c;
|
|
1215
|
-
// During the initial access check the content is already hidden by the
|
|
1216
|
-
// gate — no need to show a spinner in the paywall slot.
|
|
1217
1340
|
if (state === 'checking')
|
|
1218
1341
|
return;
|
|
1219
|
-
if (!
|
|
1342
|
+
if (!body)
|
|
1220
1343
|
init();
|
|
1221
|
-
if (!
|
|
1344
|
+
if (!body)
|
|
1222
1345
|
return;
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
overlay.removeChild(overlay.firstChild);
|
|
1346
|
+
while (body.firstChild)
|
|
1347
|
+
body.removeChild(body.firstChild);
|
|
1226
1348
|
switch (state) {
|
|
1227
1349
|
case 'login':
|
|
1228
|
-
renderLogin(
|
|
1350
|
+
renderLogin(body, callbacks);
|
|
1229
1351
|
break;
|
|
1230
1352
|
case 'purchase':
|
|
1231
|
-
renderPurchase(
|
|
1353
|
+
renderPurchase(body, callbacks, (_a = meta === null || meta === void 0 ? void 0 : meta.requiredCredits) !== null && _a !== void 0 ? _a : null);
|
|
1232
1354
|
break;
|
|
1233
1355
|
case 'insufficient':
|
|
1234
|
-
renderInsufficient(
|
|
1356
|
+
renderInsufficient(body, callbacks, (_b = meta === null || meta === void 0 ? void 0 : meta.requiredCredits) !== null && _b !== void 0 ? _b : null, (_c = meta === null || meta === void 0 ? void 0 : meta.creditBalance) !== null && _c !== void 0 ? _c : null);
|
|
1235
1357
|
break;
|
|
1236
1358
|
case 'loading':
|
|
1237
|
-
renderLoading(
|
|
1359
|
+
renderLoading(body);
|
|
1238
1360
|
break;
|
|
1239
1361
|
case 'granted':
|
|
1240
1362
|
destroy();
|
|
@@ -1242,33 +1364,44 @@ function createPaywallRenderer(config) {
|
|
|
1242
1364
|
}
|
|
1243
1365
|
}
|
|
1244
1366
|
function renderLogin(parent, cb) {
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1367
|
+
// Only show the heading/description in inline mode; in overlay mode the
|
|
1368
|
+
// client's top slot already provides the article context.
|
|
1369
|
+
if (config.paywallMode === 'inline') {
|
|
1370
|
+
parent.appendChild(el('h2', 'This article requires a subscription'));
|
|
1371
|
+
parent.appendChild(el('p', 'Log in with your Content Credits account to unlock this article.'));
|
|
1372
|
+
}
|
|
1373
|
+
const btn = el('button', 'Login & Unlock with Content Credits');
|
|
1248
1374
|
btn.className = 'cc-btn cc-btn-primary';
|
|
1249
1375
|
btn.addEventListener('click', () => { void cb.onLogin(); });
|
|
1250
1376
|
parent.appendChild(btn);
|
|
1251
1377
|
parent.appendChild(poweredBy());
|
|
1252
1378
|
}
|
|
1253
1379
|
function renderPurchase(parent, cb, credits) {
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1380
|
+
if (config.paywallMode === 'inline') {
|
|
1381
|
+
parent.appendChild(el('h2', 'Unlock this article'));
|
|
1382
|
+
if (credits !== null) {
|
|
1383
|
+
const badge = el('span', `${credits} credit${credits !== 1 ? 's' : ''}`);
|
|
1384
|
+
badge.className = 'cc-credit-badge';
|
|
1385
|
+
parent.appendChild(badge);
|
|
1386
|
+
}
|
|
1387
|
+
parent.appendChild(el('p', 'Use your Content Credits balance to instantly access this premium article.'));
|
|
1388
|
+
}
|
|
1389
|
+
const label = credits !== null
|
|
1390
|
+
? `Unlock Just This Story · ${credits} Credit${credits !== 1 ? 's' : ''}`
|
|
1391
|
+
: 'Unlock Just This Story';
|
|
1392
|
+
const btn = el('button', label);
|
|
1393
|
+
btn.className = 'cc-btn cc-btn-outline';
|
|
1263
1394
|
btn.addEventListener('click', () => { void cb.onPurchase(); });
|
|
1264
1395
|
parent.appendChild(btn);
|
|
1265
1396
|
parent.appendChild(poweredBy());
|
|
1266
1397
|
}
|
|
1267
1398
|
function renderInsufficient(parent, cb, required, available) {
|
|
1268
|
-
|
|
1399
|
+
if (config.paywallMode === 'inline') {
|
|
1400
|
+
parent.appendChild(el('h2', 'Not enough credits'));
|
|
1401
|
+
}
|
|
1269
1402
|
const detail = required !== null && available !== null
|
|
1270
|
-
? `You need ${required} credit${required !== 1 ? 's' : ''} but have ${available}
|
|
1271
|
-
: 'You don\'t have enough credits to unlock this article.
|
|
1403
|
+
? `You need ${required} credit${required !== 1 ? 's' : ''} but only have ${available}.`
|
|
1404
|
+
: 'You don\'t have enough credits to unlock this article.';
|
|
1272
1405
|
parent.appendChild(el('p', detail));
|
|
1273
1406
|
const btn = el('button', 'Buy More Credits');
|
|
1274
1407
|
btn.className = 'cc-btn cc-btn-primary';
|
|
@@ -1278,7 +1411,7 @@ function createPaywallRenderer(config) {
|
|
|
1278
1411
|
}
|
|
1279
1412
|
function renderLoading(parent) {
|
|
1280
1413
|
const btn = el('button');
|
|
1281
|
-
btn.className = 'cc-btn cc-btn-
|
|
1414
|
+
btn.className = 'cc-btn cc-btn-outline';
|
|
1282
1415
|
btn.disabled = true;
|
|
1283
1416
|
const spinner = el('span');
|
|
1284
1417
|
spinner.className = 'cc-spinner';
|
|
@@ -1298,9 +1431,9 @@ function createPaywallRenderer(config) {
|
|
|
1298
1431
|
return div;
|
|
1299
1432
|
}
|
|
1300
1433
|
function setButtonLoading(loading) {
|
|
1301
|
-
if (!
|
|
1434
|
+
if (!body)
|
|
1302
1435
|
return;
|
|
1303
|
-
const btn =
|
|
1436
|
+
const btn = body.querySelector('.cc-btn');
|
|
1304
1437
|
if (!btn)
|
|
1305
1438
|
return;
|
|
1306
1439
|
btn.disabled = loading;
|
|
@@ -1313,12 +1446,110 @@ function createPaywallRenderer(config) {
|
|
|
1313
1446
|
}
|
|
1314
1447
|
}
|
|
1315
1448
|
function destroy() {
|
|
1449
|
+
reactRoot === null || reactRoot === void 0 ? void 0 : reactRoot.unmount();
|
|
1450
|
+
reactRoot = null;
|
|
1316
1451
|
removeShadowHost(HOST_ID);
|
|
1317
1452
|
root = null;
|
|
1318
|
-
|
|
1453
|
+
body = null;
|
|
1319
1454
|
}
|
|
1320
1455
|
return { init, render, setButtonLoading, destroy };
|
|
1321
1456
|
}
|
|
1457
|
+
// ─── Top slot mounting ───────────────────────────────────────────────────────
|
|
1458
|
+
function mountTopSlot(container, slot, reactDOM) {
|
|
1459
|
+
if (!slot)
|
|
1460
|
+
return undefined;
|
|
1461
|
+
// React element — detected by the $$typeof symbol all React elements carry.
|
|
1462
|
+
if (isReactElement(slot)) {
|
|
1463
|
+
if (!reactDOM) {
|
|
1464
|
+
console.warn('[ContentCredits] paywallTopSlot is a React element but `reactDOM` was not provided. ' +
|
|
1465
|
+
'Pass your ReactDOM instance: ContentCredits.init({ reactDOM, paywallTopSlot: <Widget /> })');
|
|
1466
|
+
return undefined;
|
|
1467
|
+
}
|
|
1468
|
+
return mountReactElement(container, slot, reactDOM);
|
|
1469
|
+
}
|
|
1470
|
+
if (typeof slot === 'function') {
|
|
1471
|
+
slot(container);
|
|
1472
|
+
return undefined;
|
|
1473
|
+
}
|
|
1474
|
+
if (slot instanceof HTMLElement) {
|
|
1475
|
+
container.appendChild(slot);
|
|
1476
|
+
return undefined;
|
|
1477
|
+
}
|
|
1478
|
+
// Structured PaywallSlotItem[]
|
|
1479
|
+
for (const item of slot) {
|
|
1480
|
+
container.appendChild(renderSlotItem(item));
|
|
1481
|
+
}
|
|
1482
|
+
return undefined;
|
|
1483
|
+
}
|
|
1484
|
+
function isReactElement(value) {
|
|
1485
|
+
// All React elements (JSX) have a $$typeof symbol set to Symbol.for('react.element')
|
|
1486
|
+
// or, in older builds, the integer 0xeac7.
|
|
1487
|
+
return (typeof value === 'object' &&
|
|
1488
|
+
value !== null &&
|
|
1489
|
+
'$$typeof' in value &&
|
|
1490
|
+
!Array.isArray(value) &&
|
|
1491
|
+
!(value instanceof HTMLElement));
|
|
1492
|
+
}
|
|
1493
|
+
function mountReactElement(container, element, reactDOM) {
|
|
1494
|
+
// React 18+: createRoot
|
|
1495
|
+
if (typeof reactDOM.createRoot === 'function') {
|
|
1496
|
+
const root = reactDOM.createRoot(container);
|
|
1497
|
+
root.render(element);
|
|
1498
|
+
return root;
|
|
1499
|
+
}
|
|
1500
|
+
// React 16/17: legacy render (no unmount handle needed — it cleans up on container removal)
|
|
1501
|
+
if (typeof reactDOM.render === 'function') {
|
|
1502
|
+
reactDOM.render(element, container);
|
|
1503
|
+
return {
|
|
1504
|
+
unmount() {
|
|
1505
|
+
var _a;
|
|
1506
|
+
// ReactDOM.unmountComponentAtNode is the React 16/17 equivalent
|
|
1507
|
+
const rdom = reactDOM;
|
|
1508
|
+
(_a = rdom.unmountComponentAtNode) === null || _a === void 0 ? void 0 : _a.call(rdom, container);
|
|
1509
|
+
},
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
console.warn('[ContentCredits] The provided `reactDOM` has neither `createRoot` nor `render`. ' +
|
|
1513
|
+
'Pass a valid ReactDOM instance.');
|
|
1514
|
+
return undefined;
|
|
1515
|
+
}
|
|
1516
|
+
function renderSlotItem(item) {
|
|
1517
|
+
var _a, _b, _c, _d, _e;
|
|
1518
|
+
switch (item.type) {
|
|
1519
|
+
case 'heading': {
|
|
1520
|
+
const h = el('span', (_a = item.content) !== null && _a !== void 0 ? _a : '');
|
|
1521
|
+
h.className = 'cc-slot-heading';
|
|
1522
|
+
return h;
|
|
1523
|
+
}
|
|
1524
|
+
case 'subheading': {
|
|
1525
|
+
const h = el('span', (_b = item.content) !== null && _b !== void 0 ? _b : '');
|
|
1526
|
+
h.className = 'cc-slot-subheading';
|
|
1527
|
+
return h;
|
|
1528
|
+
}
|
|
1529
|
+
case 'text': {
|
|
1530
|
+
const p = el('span', (_c = item.content) !== null && _c !== void 0 ? _c : '');
|
|
1531
|
+
p.className = 'cc-slot-text';
|
|
1532
|
+
return p;
|
|
1533
|
+
}
|
|
1534
|
+
case 'button': {
|
|
1535
|
+
const btn = el('button', (_d = item.content) !== null && _d !== void 0 ? _d : '');
|
|
1536
|
+
const variantClass = item.variant === 'outline'
|
|
1537
|
+
? 'cc-btn-outline'
|
|
1538
|
+
: item.variant === 'secondary'
|
|
1539
|
+
? 'cc-btn-secondary'
|
|
1540
|
+
: 'cc-btn-primary';
|
|
1541
|
+
btn.className = `cc-btn ${variantClass}`;
|
|
1542
|
+
if (item.onClick)
|
|
1543
|
+
btn.addEventListener('click', item.onClick);
|
|
1544
|
+
return btn;
|
|
1545
|
+
}
|
|
1546
|
+
case 'divider': {
|
|
1547
|
+
const d = el('div', (_e = item.content) !== null && _e !== void 0 ? _e : '');
|
|
1548
|
+
d.className = 'cc-slot-divider';
|
|
1549
|
+
return d;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1322
1553
|
|
|
1323
1554
|
const DETECTION_TIMEOUT_MS = 2000;
|
|
1324
1555
|
/**
|
|
@@ -1652,12 +1883,18 @@ function openAuthPopup(authUrl) {
|
|
|
1652
1883
|
});
|
|
1653
1884
|
}
|
|
1654
1885
|
|
|
1886
|
+
// How long to wait for the extension to respond to an authorization request
|
|
1887
|
+
// before falling back to the direct API check. MV3 service workers can take
|
|
1888
|
+
// a moment to wake up, but if they don't respond within this window we
|
|
1889
|
+
// assume the extension isn't functional and proceed without it.
|
|
1890
|
+
const EXTENSION_RESPONSE_TIMEOUT_MS = 3000;
|
|
1655
1891
|
function createPaywall(config, creditsApi, state, emitter, existingGate) {
|
|
1656
1892
|
// Accept a pre-created gate so the caller can call gate.hide() synchronously
|
|
1657
1893
|
// before any async work, preventing a flash of the full article content.
|
|
1658
1894
|
const gate = existingGate !== null && existingGate !== void 0 ? existingGate : createGate({
|
|
1659
1895
|
selector: config.contentSelector,
|
|
1660
1896
|
teaserParagraphs: config.teaserParagraphs,
|
|
1897
|
+
paywallMode: config.paywallMode,
|
|
1661
1898
|
});
|
|
1662
1899
|
const renderer = createPaywallRenderer(config);
|
|
1663
1900
|
const bridge = createExtensionBridge();
|
|
@@ -1768,6 +2005,43 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
|
|
|
1768
2005
|
function doBuyMoreCredits() {
|
|
1769
2006
|
window.open(`${"https://accounts.contentcredits.com"}/consumer/dashboard`, '_blank', 'noopener,noreferrer');
|
|
1770
2007
|
}
|
|
2008
|
+
// ── Extension auth response handler ──────────────────────────────────────
|
|
2009
|
+
function handleExtensionAuthResponse(data) {
|
|
2010
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
2011
|
+
state.set({
|
|
2012
|
+
isLoggedIn: data.isAuthenticated,
|
|
2013
|
+
hasAccess: data.doesHaveAccess,
|
|
2014
|
+
isLoaded: true,
|
|
2015
|
+
isLoading: false,
|
|
2016
|
+
creditBalance: (_a = data.creditBalance) !== null && _a !== void 0 ? _a : null,
|
|
2017
|
+
requiredCredits: (_b = data.requiredCredits) !== null && _b !== void 0 ? _b : null,
|
|
2018
|
+
});
|
|
2019
|
+
if (!data.isAuthenticated) {
|
|
2020
|
+
if (!config.headless) {
|
|
2021
|
+
gate.hide();
|
|
2022
|
+
renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
2023
|
+
}
|
|
2024
|
+
(_c = config.onLoginRequired) === null || _c === void 0 ? void 0 : _c.call(config);
|
|
2025
|
+
emitter.emit('paywall:shown', {});
|
|
2026
|
+
}
|
|
2027
|
+
else if (data.doesHaveAccess) {
|
|
2028
|
+
handleAccessGranted(0, (_d = data.creditBalance) !== null && _d !== void 0 ? _d : 0);
|
|
2029
|
+
}
|
|
2030
|
+
else {
|
|
2031
|
+
if (!config.headless) {
|
|
2032
|
+
gate.hide();
|
|
2033
|
+
renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits }, {
|
|
2034
|
+
requiredCredits: data.requiredCredits,
|
|
2035
|
+
creditBalance: data.creditBalance,
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
(_e = config.onPurchaseRequired) === null || _e === void 0 ? void 0 : _e.call(config, {
|
|
2039
|
+
requiredCredits: (_f = data.requiredCredits) !== null && _f !== void 0 ? _f : null,
|
|
2040
|
+
creditBalance: (_g = data.creditBalance) !== null && _g !== void 0 ? _g : null,
|
|
2041
|
+
});
|
|
2042
|
+
emitter.emit('paywall:shown', {});
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
1771
2045
|
// ── Access Check ──────────────────────────────────────────────────────────
|
|
1772
2046
|
async function checkAccess() {
|
|
1773
2047
|
var _a, _b, _c;
|
|
@@ -1775,8 +2049,26 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
|
|
|
1775
2049
|
if (!config.headless)
|
|
1776
2050
|
renderer.render('checking', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
1777
2051
|
if (extensionAvailable) {
|
|
1778
|
-
|
|
1779
|
-
|
|
2052
|
+
// Race the extension response against a timeout. MV3 service workers can
|
|
2053
|
+
// be asleep and take time to wake — if they don't respond in time we mark
|
|
2054
|
+
// the extension as non-functional and fall through to the API check so the
|
|
2055
|
+
// logged-in user isn't left stuck on a blank/hidden article.
|
|
2056
|
+
const responded = await new Promise(resolve => {
|
|
2057
|
+
const timer = setTimeout(() => {
|
|
2058
|
+
extensionAvailable = false;
|
|
2059
|
+
state.set({ isExtensionAvailable: false });
|
|
2060
|
+
resolve(false);
|
|
2061
|
+
}, EXTENSION_RESPONSE_TIMEOUT_MS);
|
|
2062
|
+
bridge.onAuthorizationResponse(data => {
|
|
2063
|
+
clearTimeout(timer);
|
|
2064
|
+
handleExtensionAuthResponse(data);
|
|
2065
|
+
resolve(true);
|
|
2066
|
+
});
|
|
2067
|
+
bridge.requestAuthorization(config.apiKey, config.hostName);
|
|
2068
|
+
});
|
|
2069
|
+
if (responded)
|
|
2070
|
+
return;
|
|
2071
|
+
// Extension timed out — fall through to direct API check below.
|
|
1780
2072
|
}
|
|
1781
2073
|
if (!tokenStorage.has()) {
|
|
1782
2074
|
state.set({ isLoading: false, isLoaded: true });
|
|
@@ -1842,42 +2134,6 @@ function createPaywall(config, creditsApi, state, emitter, existingGate) {
|
|
|
1842
2134
|
state.set({ isExtensionAvailable: extensionAvailable });
|
|
1843
2135
|
if (extensionAvailable) {
|
|
1844
2136
|
bridge.attach();
|
|
1845
|
-
bridge.onAuthorizationResponse(data => {
|
|
1846
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
1847
|
-
state.set({
|
|
1848
|
-
isLoggedIn: data.isAuthenticated,
|
|
1849
|
-
hasAccess: data.doesHaveAccess,
|
|
1850
|
-
isLoaded: true,
|
|
1851
|
-
isLoading: false,
|
|
1852
|
-
creditBalance: (_a = data.creditBalance) !== null && _a !== void 0 ? _a : null,
|
|
1853
|
-
requiredCredits: (_b = data.requiredCredits) !== null && _b !== void 0 ? _b : null,
|
|
1854
|
-
});
|
|
1855
|
-
if (!data.isAuthenticated) {
|
|
1856
|
-
if (!config.headless) {
|
|
1857
|
-
gate.hide();
|
|
1858
|
-
renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
1859
|
-
}
|
|
1860
|
-
(_c = config.onLoginRequired) === null || _c === void 0 ? void 0 : _c.call(config);
|
|
1861
|
-
emitter.emit('paywall:shown', {});
|
|
1862
|
-
}
|
|
1863
|
-
else if (data.doesHaveAccess) {
|
|
1864
|
-
handleAccessGranted(0, (_d = data.creditBalance) !== null && _d !== void 0 ? _d : 0);
|
|
1865
|
-
}
|
|
1866
|
-
else {
|
|
1867
|
-
if (!config.headless) {
|
|
1868
|
-
gate.hide();
|
|
1869
|
-
renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits }, {
|
|
1870
|
-
requiredCredits: data.requiredCredits,
|
|
1871
|
-
creditBalance: data.creditBalance,
|
|
1872
|
-
});
|
|
1873
|
-
}
|
|
1874
|
-
(_e = config.onPurchaseRequired) === null || _e === void 0 ? void 0 : _e.call(config, {
|
|
1875
|
-
requiredCredits: (_f = data.requiredCredits) !== null && _f !== void 0 ? _f : null,
|
|
1876
|
-
creditBalance: (_g = data.creditBalance) !== null && _g !== void 0 ? _g : null,
|
|
1877
|
-
});
|
|
1878
|
-
emitter.emit('paywall:shown', {});
|
|
1879
|
-
}
|
|
1880
|
-
});
|
|
1881
2137
|
bridge.onPurchaseResponse(data => {
|
|
1882
2138
|
var _a, _b;
|
|
1883
2139
|
state.set({ isLoading: false, isLoaded: true, hasAccess: data.doesHaveAccess });
|
|
@@ -2797,6 +3053,7 @@ class ContentCredits {
|
|
|
2797
3053
|
const earlyGate = createGate({
|
|
2798
3054
|
selector: this.config.contentSelector,
|
|
2799
3055
|
teaserParagraphs: this.config.teaserParagraphs,
|
|
3056
|
+
paywallMode: this.config.paywallMode,
|
|
2800
3057
|
});
|
|
2801
3058
|
if (!this.config.headless)
|
|
2802
3059
|
earlyGate.hide();
|
|
@@ -2952,7 +3209,7 @@ class ContentCredits {
|
|
|
2952
3209
|
}
|
|
2953
3210
|
/** SDK version string. */
|
|
2954
3211
|
static get version() {
|
|
2955
|
-
return "2.
|
|
3212
|
+
return "2.13.0";
|
|
2956
3213
|
}
|
|
2957
3214
|
}
|
|
2958
3215
|
// ── Auto-init from script data attributes (CDN usage) ────────────────────────
|