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