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