@contentcredits/sdk 2.0.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.
@@ -0,0 +1,2574 @@
1
+ 'use strict';
2
+
3
+ function resolveConfig(raw) {
4
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
5
+ if (!raw.apiKey || typeof raw.apiKey !== 'string' || raw.apiKey.trim() === '') {
6
+ throw new Error('[ContentCredits] apiKey is required. Get yours from the Content Credits admin panel.');
7
+ }
8
+ const articleUrl = (_a = raw.articleUrl) !== null && _a !== void 0 ? _a : window.location.href;
9
+ let hostName;
10
+ try {
11
+ hostName = new URL(articleUrl).hostname;
12
+ }
13
+ catch (_l) {
14
+ throw new Error(`[ContentCredits] Invalid articleUrl: "${articleUrl}"`);
15
+ }
16
+ return {
17
+ apiKey: raw.apiKey.trim(),
18
+ articleUrl,
19
+ hostName,
20
+ pageTitle: document.title,
21
+ contentSelector: (_b = raw.contentSelector) !== null && _b !== void 0 ? _b : '.cc-premium-content',
22
+ teaserParagraphs: (_c = raw.teaserParagraphs) !== null && _c !== void 0 ? _c : 2,
23
+ enableComments: (_d = raw.enableComments) !== null && _d !== void 0 ? _d : true,
24
+ extensionId: (_e = raw.extensionId) !== null && _e !== void 0 ? _e : "ljehdpabbhgccmanhjdfacjnaigpgcml",
25
+ debug: (_f = raw.debug) !== null && _f !== void 0 ? _f : false,
26
+ apiBaseUrl: "https://api.contentcredits.com",
27
+ accountsUrl: "https://accounts.contentcredits.com",
28
+ paywallTemplate: raw.paywallTemplate,
29
+ onAccessGranted: raw.onAccessGranted,
30
+ theme: {
31
+ primaryColor: (_h = (_g = raw.theme) === null || _g === void 0 ? void 0 : _g.primaryColor) !== null && _h !== void 0 ? _h : '#44C678',
32
+ fontFamily: (_k = (_j = raw.theme) === null || _j === void 0 ? void 0 : _j.fontFamily) !== null && _k !== void 0 ? _k : "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
33
+ },
34
+ };
35
+ }
36
+
37
+ function createInitialState() {
38
+ return {
39
+ isLoading: false,
40
+ isExtensionAvailable: false,
41
+ isLoggedIn: false,
42
+ hasAccess: false,
43
+ isLoaded: false,
44
+ user: null,
45
+ creditBalance: null,
46
+ requiredCredits: null,
47
+ };
48
+ }
49
+ function createState() {
50
+ let current = createInitialState();
51
+ const subscribers = [];
52
+ function get() {
53
+ return Object.assign({}, current);
54
+ }
55
+ function set(patch) {
56
+ current = Object.assign(Object.assign({}, current), patch);
57
+ subscribers.forEach(fn => fn(get()));
58
+ }
59
+ function subscribe(fn) {
60
+ subscribers.push(fn);
61
+ return () => {
62
+ const i = subscribers.indexOf(fn);
63
+ if (i >= 0)
64
+ subscribers.splice(i, 1);
65
+ };
66
+ }
67
+ function reset() {
68
+ current = createInitialState();
69
+ subscribers.forEach(fn => fn(get()));
70
+ }
71
+ function setUser(user) {
72
+ var _a;
73
+ set({
74
+ user,
75
+ isLoggedIn: user !== null,
76
+ creditBalance: (_a = user === null || user === void 0 ? void 0 : user.credits) !== null && _a !== void 0 ? _a : null,
77
+ });
78
+ }
79
+ return { get, set, subscribe, reset, setUser };
80
+ }
81
+
82
+ function createEventEmitter() {
83
+ const listeners = {};
84
+ function on(event, handler) {
85
+ if (!listeners[event]) {
86
+ listeners[event] = [];
87
+ }
88
+ listeners[event].push(handler);
89
+ // Return unsubscribe function
90
+ return () => off(event, handler);
91
+ }
92
+ function off(event, handler) {
93
+ const arr = listeners[event];
94
+ if (!arr)
95
+ return;
96
+ const idx = arr.indexOf(handler);
97
+ if (idx >= 0)
98
+ arr.splice(idx, 1);
99
+ }
100
+ function emit(event, payload) {
101
+ const arr = listeners[event];
102
+ if (arr) {
103
+ arr.forEach(handler => {
104
+ try {
105
+ handler(payload);
106
+ }
107
+ catch (e) {
108
+ console.warn(`[ContentCredits] Error in "${event}" handler:`, e);
109
+ }
110
+ });
111
+ }
112
+ // Also dispatch as a native CustomEvent on document so vanilla listeners work
113
+ try {
114
+ document.dispatchEvent(new CustomEvent(`contentcredits:${event}`, { detail: payload, bubbles: false }));
115
+ }
116
+ catch (_a) {
117
+ // ignore environments without CustomEvent
118
+ }
119
+ }
120
+ function removeAll() {
121
+ Object.keys(listeners).forEach(k => {
122
+ delete listeners[k];
123
+ });
124
+ }
125
+ return { on, off, emit, removeAll };
126
+ }
127
+
128
+ /**
129
+ * Decode a JWT payload without verifying the signature.
130
+ * Signature verification happens server-side on every API call.
131
+ */
132
+ function decodeJwt(token) {
133
+ try {
134
+ const parts = token.split('.');
135
+ if (parts.length !== 3)
136
+ return null;
137
+ const base64Url = parts[1];
138
+ // Normalise Base64URL → Base64
139
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
140
+ const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
141
+ const json = decodeURIComponent(atob(padded)
142
+ .split('')
143
+ .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
144
+ .join(''));
145
+ return JSON.parse(json);
146
+ }
147
+ catch (_a) {
148
+ return null;
149
+ }
150
+ }
151
+ /** Returns true if the JWT is expired (or unparseable). */
152
+ function isTokenExpired(token) {
153
+ const payload = decodeJwt(token);
154
+ if (!payload || typeof payload.exp !== 'number')
155
+ return true;
156
+ // exp is in seconds; compare against current time in seconds
157
+ return Date.now() / 1000 > payload.exp;
158
+ }
159
+ /** Extract the user ID from a JWT. Returns null if token is invalid. */
160
+ function getUserIdFromToken(token) {
161
+ var _a, _b;
162
+ const payload = decodeJwt(token);
163
+ return payload ? ((_b = (_a = payload.id) !== null && _a !== void 0 ? _a : payload._id) !== null && _b !== void 0 ? _b : null) : null;
164
+ }
165
+
166
+ const SESSION_KEY = 'cc_sdk_token';
167
+ /**
168
+ * Two-layer token storage:
169
+ * 1. In-memory (primary) — invisible to other scripts, survives page navigations
170
+ * within the same JS context but gone on hard reload.
171
+ * 2. sessionStorage (secondary) — survives soft reloads, cleared when the tab
172
+ * closes, never shared across tabs.
173
+ *
174
+ * We intentionally never use document.cookie (no HttpOnly = XSS risk) or
175
+ * localStorage (persists indefinitely across sessions).
176
+ */
177
+ let memoryToken = null;
178
+ const tokenStorage = {
179
+ set(token) {
180
+ memoryToken = token;
181
+ try {
182
+ sessionStorage.setItem(SESSION_KEY, token);
183
+ }
184
+ catch (_a) {
185
+ // sessionStorage unavailable (e.g. private mode with strict settings) — ok
186
+ }
187
+ },
188
+ get() {
189
+ // Memory hit
190
+ if (memoryToken) {
191
+ if (isTokenExpired(memoryToken)) {
192
+ this.clear();
193
+ return null;
194
+ }
195
+ return memoryToken;
196
+ }
197
+ // sessionStorage fallback (page reloaded but tab still open)
198
+ try {
199
+ const stored = sessionStorage.getItem(SESSION_KEY);
200
+ if (stored) {
201
+ if (isTokenExpired(stored)) {
202
+ this.clear();
203
+ return null;
204
+ }
205
+ memoryToken = stored; // warm up memory layer
206
+ return stored;
207
+ }
208
+ }
209
+ catch (_a) {
210
+ // ignore
211
+ }
212
+ return null;
213
+ },
214
+ clear() {
215
+ memoryToken = null;
216
+ try {
217
+ sessionStorage.removeItem(SESSION_KEY);
218
+ }
219
+ catch (_a) {
220
+ // ignore
221
+ }
222
+ },
223
+ has() {
224
+ return this.get() !== null;
225
+ },
226
+ };
227
+
228
+ const REQUEST_TIMEOUT_MS = 12000;
229
+ const MAX_RETRIES = 3;
230
+ const RETRY_DELAY_MS = 400;
231
+ /** Simple hash for deduplication key */
232
+ function requestKey(method, url, body) {
233
+ return `${method}:${url}:${body !== null && body !== void 0 ? body : ''}`;
234
+ }
235
+ const inFlight = new Map();
236
+ async function sleep(ms) {
237
+ return new Promise(resolve => setTimeout(resolve, ms));
238
+ }
239
+ function shouldRetry(status) {
240
+ return status >= 500 || status === 429;
241
+ }
242
+ function createApiClient(baseUrl, emitter) {
243
+ async function request(method, path, body, attempt = 0) {
244
+ const url = `${baseUrl}${path}`;
245
+ const serialisedBody = body ? JSON.stringify(body) : undefined;
246
+ const key = requestKey(method, url, serialisedBody);
247
+ // Deduplicate concurrent identical requests
248
+ const existing = inFlight.get(key);
249
+ if (existing)
250
+ return existing;
251
+ const controller = new AbortController();
252
+ const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
253
+ const headers = {
254
+ 'Content-Type': 'application/json',
255
+ };
256
+ const token = tokenStorage.get();
257
+ if (token) {
258
+ headers['Authorization'] = `Bearer ${token}`;
259
+ }
260
+ const promise = fetch(url, {
261
+ method,
262
+ headers,
263
+ body: serialisedBody,
264
+ signal: controller.signal,
265
+ credentials: 'omit', // SDK uses explicit Bearer header, not cookies
266
+ })
267
+ .then(async (response) => {
268
+ var _a;
269
+ clearTimeout(timeoutId);
270
+ if (response.status === 401) {
271
+ tokenStorage.clear();
272
+ emitter.emit('auth:logout', {});
273
+ throw new ApiError(401, 'Unauthorized — session expired');
274
+ }
275
+ let data;
276
+ try {
277
+ data = await response.json();
278
+ }
279
+ catch (_b) {
280
+ throw new ApiError(response.status, 'Invalid JSON response from server');
281
+ }
282
+ if (!response.ok) {
283
+ const msg = (_a = data === null || data === void 0 ? void 0 : data.message) !== null && _a !== void 0 ? _a : `HTTP ${response.status}`;
284
+ throw new ApiError(response.status, msg, data);
285
+ }
286
+ return data;
287
+ })
288
+ .catch(async (err) => {
289
+ clearTimeout(timeoutId);
290
+ // Retry on network error or server error (not client error)
291
+ const isNetworkError = err instanceof TypeError && err.message.includes('fetch');
292
+ const isServerError = err instanceof ApiError && shouldRetry(err.status);
293
+ if ((isNetworkError || isServerError) && attempt < MAX_RETRIES) {
294
+ inFlight.delete(key);
295
+ await sleep(RETRY_DELAY_MS * Math.pow(2, attempt));
296
+ return request(method, path, body, attempt + 1);
297
+ }
298
+ throw err;
299
+ })
300
+ .finally(() => {
301
+ inFlight.delete(key);
302
+ });
303
+ inFlight.set(key, promise);
304
+ return promise;
305
+ }
306
+ return {
307
+ get: (path) => request('GET', path),
308
+ post: (path, body) => request('POST', path, body),
309
+ put: (path, body) => request('PUT', path, body),
310
+ delete: (path) => request('DELETE', path),
311
+ };
312
+ }
313
+ class ApiError extends Error {
314
+ constructor(status, message, data) {
315
+ super(message);
316
+ this.status = status;
317
+ this.data = data;
318
+ this.name = 'ApiError';
319
+ }
320
+ }
321
+
322
+ function createCreditsApi(client) {
323
+ return {
324
+ checkAccess(params) {
325
+ return client.post('/credits/check-article-access', {
326
+ apiKey: params.apiKey,
327
+ postUrl: params.postUrl,
328
+ postName: params.postName,
329
+ hostName: params.hostName,
330
+ });
331
+ },
332
+ purchaseArticle(params) {
333
+ return client.post('/credits/purchase-article', {
334
+ apiKey: params.apiKey,
335
+ postUrl: params.postUrl,
336
+ postName: params.postName,
337
+ hostName: params.hostName,
338
+ });
339
+ },
340
+ };
341
+ }
342
+
343
+ function createCommentsApi(client) {
344
+ return {
345
+ // Backend returns the thread object directly (no success wrapper)
346
+ ensureThread(params) {
347
+ return client.post('/comments/threads/ensure', {
348
+ pageUrl: params.pageUrl,
349
+ hostname: params.hostname,
350
+ });
351
+ },
352
+ // Backend returns { thread, comments } — no success wrapper
353
+ getComments(params) {
354
+ const encoded = encodeURIComponent(params.pageUrl);
355
+ return client.get(`/comments/comments/by-url?url=${encoded}&sortBy=${params.sortBy}`);
356
+ },
357
+ // Backend returns the created comment object directly
358
+ postComment(params) {
359
+ return client.post('/comments/comments', Object.assign({ threadId: params.threadId, content: params.content }, (params.parentCommentId ? { parentCommentId: params.parentCommentId } : {})));
360
+ },
361
+ // Backend returns the updated comment object directly
362
+ editComment(commentId, content) {
363
+ return client.put(`/comments/comments/${commentId}`, { content });
364
+ },
365
+ // Backend returns the deleted comment object directly
366
+ deleteComment(commentId) {
367
+ return client.delete(`/comments/comments/${commentId}`);
368
+ },
369
+ // Backend returns { success: true, data: { _id, likeCount, hasLiked } }
370
+ toggleLike(commentId) {
371
+ return client.post(`/comments/comments/${commentId}/toggle-like`, {});
372
+ },
373
+ };
374
+ }
375
+
376
+ const GATE_ATTR = 'data-cc-gated';
377
+ function createGate(options) {
378
+ let gated = false;
379
+ let contentEl = null;
380
+ let hiddenNodes = [];
381
+ function findContent() {
382
+ return document.querySelector(options.selector);
383
+ }
384
+ function hide() {
385
+ contentEl = findContent();
386
+ if (!contentEl)
387
+ return false;
388
+ if (contentEl.hasAttribute(GATE_ATTR))
389
+ return true; // already gated
390
+ const paragraphs = Array.from(contentEl.querySelectorAll('p, h2, h3, h4, blockquote, ul, ol'));
391
+ // Collect nodes to hide (everything after the teaser threshold)
392
+ if (paragraphs.length > options.teaserParagraphs) {
393
+ const hideFrom = paragraphs[options.teaserParagraphs];
394
+ const childNodes = Array.from(contentEl.childNodes);
395
+ const pivotIndex = childNodes.findIndex(n => n === hideFrom || contentEl.contains(n) && n.compareDocumentPosition(hideFrom) & Node.DOCUMENT_POSITION_FOLLOWING);
396
+ hiddenNodes = childNodes.slice(pivotIndex < 0 ? options.teaserParagraphs : pivotIndex);
397
+ hiddenNodes.forEach(n => {
398
+ var _a, _b, _c, _d;
399
+ if (n instanceof HTMLElement || n instanceof Text) {
400
+ (_b = (_a = n.style) === null || _a === void 0 ? void 0 : _a.setProperty) === null || _b === void 0 ? void 0 : _b.call(_a, 'display', 'none');
401
+ (_d = (_c = n).setAttribute) === null || _d === void 0 ? void 0 : _d.call(_c, 'data-cc-hidden', 'true');
402
+ }
403
+ });
404
+ }
405
+ else {
406
+ // Not enough paragraphs to split — hide the entire content
407
+ hiddenNodes = Array.from(contentEl.childNodes);
408
+ hiddenNodes.forEach(n => {
409
+ if (n instanceof HTMLElement)
410
+ n.style.display = 'none';
411
+ });
412
+ }
413
+ contentEl.setAttribute(GATE_ATTR, 'true');
414
+ gated = true;
415
+ return true;
416
+ }
417
+ function reveal() {
418
+ if (!gated)
419
+ return;
420
+ hiddenNodes.forEach(n => {
421
+ if (n instanceof HTMLElement) {
422
+ n.style.removeProperty('display');
423
+ n.removeAttribute('data-cc-hidden');
424
+ }
425
+ });
426
+ hiddenNodes = [];
427
+ contentEl === null || contentEl === void 0 ? void 0 : contentEl.removeAttribute(GATE_ATTR);
428
+ gated = false;
429
+ }
430
+ function isGated() {
431
+ return gated;
432
+ }
433
+ return { hide, reveal, isGated };
434
+ }
435
+
436
+ /**
437
+ * Creates an isolated Shadow DOM host attached to document.body.
438
+ * All SDK UI lives inside this shadow root so partner CSS cannot
439
+ * bleed in and SDK CSS cannot bleed out.
440
+ */
441
+ function createShadowHost(id) {
442
+ // Reuse if already in the DOM (e.g. hot-reload)
443
+ let host = document.getElementById(id);
444
+ if (!host) {
445
+ host = document.createElement('div');
446
+ host.id = id;
447
+ // The host element itself is invisible; only its shadow children show
448
+ host.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;pointer-events:none;z-index:2147483647;';
449
+ document.body.appendChild(host);
450
+ }
451
+ const existing = host._ccShadow;
452
+ if (existing)
453
+ return { host, root: existing };
454
+ const root = host.attachShadow({ mode: 'open' });
455
+ host._ccShadow = root;
456
+ return { host, root };
457
+ }
458
+ function removeShadowHost(id) {
459
+ const host = document.getElementById(id);
460
+ if (host)
461
+ host.remove();
462
+ }
463
+ /** Inject a <style> tag into a shadow root */
464
+ function injectStyles(root, css) {
465
+ const existing = root.querySelector('style[data-cc-styles]');
466
+ if (existing) {
467
+ existing.textContent = css;
468
+ return;
469
+ }
470
+ const style = document.createElement('style');
471
+ style.dataset.ccStyles = 'true';
472
+ style.textContent = css;
473
+ root.appendChild(style);
474
+ }
475
+
476
+ function getPaywallStyles(primaryColor, fontFamily) {
477
+ return `
478
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
479
+
480
+ .cc-paywall-gate {
481
+ position: relative;
482
+ width: 100%;
483
+ }
484
+
485
+ .cc-teaser-fade {
486
+ position: relative;
487
+ }
488
+ .cc-teaser-fade::after {
489
+ content: '';
490
+ position: absolute;
491
+ bottom: 0; left: 0; width: 100%;
492
+ height: 120px;
493
+ background: linear-gradient(to bottom, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
494
+ pointer-events: none;
495
+ }
496
+
497
+ .cc-paywall-overlay {
498
+ background: #fff;
499
+ border: 1px solid #e5e7eb;
500
+ border-radius: 12px;
501
+ padding: 32px 24px;
502
+ max-width: 480px;
503
+ margin: 24px auto;
504
+ text-align: center;
505
+ font-family: ${fontFamily};
506
+ box-shadow: 0 4px 24px rgba(0,0,0,0.08);
507
+ }
508
+
509
+ .cc-paywall-overlay h2 {
510
+ font-size: 20px;
511
+ font-weight: 700;
512
+ color: #111827;
513
+ margin-bottom: 8px;
514
+ }
515
+
516
+ .cc-paywall-overlay p {
517
+ font-size: 14px;
518
+ color: #6b7280;
519
+ margin-bottom: 24px;
520
+ line-height: 1.6;
521
+ }
522
+
523
+ .cc-btn {
524
+ display: inline-flex;
525
+ align-items: center;
526
+ justify-content: center;
527
+ gap: 8px;
528
+ height: 44px;
529
+ padding: 0 20px;
530
+ border: none;
531
+ border-radius: 8px;
532
+ font-family: ${fontFamily};
533
+ font-size: 15px;
534
+ font-weight: 600;
535
+ cursor: pointer;
536
+ transition: opacity 0.15s ease, transform 0.1s ease;
537
+ width: 100%;
538
+ max-width: 320px;
539
+ }
540
+ .cc-btn:hover:not(:disabled) { opacity: 0.88; }
541
+ .cc-btn:active:not(:disabled) { transform: scale(0.98); }
542
+ .cc-btn:disabled { opacity: 0.55; cursor: not-allowed; }
543
+
544
+ .cc-btn-primary {
545
+ background: ${primaryColor};
546
+ color: #fff;
547
+ }
548
+
549
+ .cc-btn-secondary {
550
+ background: #111827;
551
+ color: #fff;
552
+ margin-top: 10px;
553
+ }
554
+
555
+ .cc-btn-outline {
556
+ background: transparent;
557
+ color: #111827;
558
+ border: 2px solid #111827;
559
+ margin-top: 10px;
560
+ }
561
+
562
+ .cc-credit-badge {
563
+ display: inline-block;
564
+ background: #fef3c7;
565
+ color: #92400e;
566
+ border-radius: 20px;
567
+ padding: 2px 10px;
568
+ font-size: 13px;
569
+ font-weight: 600;
570
+ margin-bottom: 16px;
571
+ }
572
+
573
+ .cc-spinner {
574
+ width: 18px; height: 18px;
575
+ border: 2px solid rgba(255,255,255,0.4);
576
+ border-top-color: #fff;
577
+ border-radius: 50%;
578
+ animation: cc-spin 0.7s linear infinite;
579
+ flex-shrink: 0;
580
+ }
581
+ @keyframes cc-spin { to { transform: rotate(360deg); } }
582
+
583
+ .cc-powered-by {
584
+ margin-top: 20px;
585
+ font-size: 12px;
586
+ color: #9ca3af;
587
+ }
588
+ .cc-powered-by a {
589
+ color: ${primaryColor};
590
+ text-decoration: none;
591
+ font-weight: 600;
592
+ }
593
+ .cc-powered-by a:hover { text-decoration: underline; }
594
+ `;
595
+ }
596
+ function getCommentStyles(primaryColor, fontFamily) {
597
+ return `
598
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
599
+
600
+ /* ── Widget Button ───────────────────────────────────── */
601
+ .cc-widget-btn {
602
+ position: fixed;
603
+ top: 50%;
604
+ right: 0;
605
+ transform: translateY(-50%);
606
+ width: auto;
607
+ height: 60px;
608
+ background: ${primaryColor};
609
+ border-radius: 10px 0 0 10px;
610
+ display: flex;
611
+ align-items: center;
612
+ gap: 8px;
613
+ padding-left: 12px;
614
+ padding-right: 6px;
615
+ z-index: 2147483646;
616
+ box-shadow: -2px 0 16px rgba(0,0,0,0.12);
617
+ cursor: pointer;
618
+ user-select: none;
619
+ font-family: ${fontFamily};
620
+ transition: background 0.2s;
621
+ pointer-events: all;
622
+ }
623
+ .cc-widget-btn:hover { filter: brightness(1.08); }
624
+
625
+ .cc-widget-icon { color: #fff; display: flex; align-items: center; }
626
+ .cc-widget-badge {
627
+ background: #fff;
628
+ color: ${primaryColor};
629
+ border-radius: 12px;
630
+ padding: 2px 7px;
631
+ font-size: 12px;
632
+ font-weight: 700;
633
+ min-width: 20px;
634
+ text-align: center;
635
+ }
636
+ .cc-widget-drag-handle {
637
+ color: rgba(255,255,255,0.7);
638
+ cursor: grab;
639
+ display: flex;
640
+ align-items: center;
641
+ padding: 0 4px;
642
+ }
643
+ .cc-widget-drag-handle:active { cursor: grabbing; }
644
+
645
+ /* ── Panel Overlay ───────────────────────────────────── */
646
+ .cc-panel-backdrop {
647
+ position: fixed;
648
+ inset: 0;
649
+ background: rgba(0,0,0,0.35);
650
+ z-index: 2147483645;
651
+ opacity: 0;
652
+ transition: opacity 0.25s;
653
+ pointer-events: all;
654
+ }
655
+ .cc-panel-backdrop.cc-visible { opacity: 1; }
656
+
657
+ .cc-panel {
658
+ position: fixed;
659
+ top: 0; right: -500px;
660
+ width: 460px;
661
+ max-width: 95vw;
662
+ height: 100%;
663
+ background: #f9fafb;
664
+ z-index: 2147483646;
665
+ display: flex;
666
+ flex-direction: column;
667
+ box-shadow: -4px 0 32px rgba(0,0,0,0.12);
668
+ transition: right 0.28s cubic-bezier(0.4, 0, 0.2, 1);
669
+ font-family: ${fontFamily};
670
+ pointer-events: all;
671
+ }
672
+ .cc-panel.cc-open { right: 0; }
673
+
674
+ /* ── Panel Header ────────────────────────────────────── */
675
+ .cc-panel-header {
676
+ display: flex;
677
+ align-items: center;
678
+ gap: 10px;
679
+ padding: 16px 20px;
680
+ border-bottom: 1px solid #e5e7eb;
681
+ background: #fff;
682
+ flex-shrink: 0;
683
+ }
684
+ .cc-panel-title {
685
+ font-size: 16px;
686
+ font-weight: 700;
687
+ color: #111827;
688
+ flex: 1;
689
+ }
690
+ .cc-panel-count {
691
+ font-size: 13px;
692
+ color: #6b7280;
693
+ font-weight: 400;
694
+ }
695
+ .cc-panel-close-btn {
696
+ background: transparent;
697
+ border: none;
698
+ cursor: pointer;
699
+ color: #6b7280;
700
+ padding: 4px;
701
+ display: flex;
702
+ align-items: center;
703
+ border-radius: 6px;
704
+ transition: background 0.15s;
705
+ }
706
+ .cc-panel-close-btn:hover { background: #f3f4f6; }
707
+
708
+ .cc-back-btn {
709
+ background: transparent;
710
+ border: none;
711
+ cursor: pointer;
712
+ color: #6b7280;
713
+ padding: 4px;
714
+ display: none;
715
+ align-items: center;
716
+ border-radius: 6px;
717
+ transition: background 0.15s;
718
+ }
719
+ .cc-back-btn.cc-visible { display: flex; }
720
+ .cc-back-btn:hover { background: #f3f4f6; }
721
+
722
+ /* ── Sort Bar ────────────────────────────────────────── */
723
+ .cc-sort-bar {
724
+ display: flex;
725
+ align-items: center;
726
+ gap: 6px;
727
+ padding: 10px 20px;
728
+ border-bottom: 1px solid #f3f4f6;
729
+ background: #fff;
730
+ flex-shrink: 0;
731
+ }
732
+ .cc-sort-label { font-size: 12px; color: #9ca3af; font-weight: 500; }
733
+ .cc-sort-btn {
734
+ background: transparent;
735
+ border: none;
736
+ cursor: pointer;
737
+ font-size: 12px;
738
+ font-weight: 600;
739
+ color: #6b7280;
740
+ padding: 4px 8px;
741
+ border-radius: 6px;
742
+ font-family: ${fontFamily};
743
+ transition: background 0.15s, color 0.15s;
744
+ }
745
+ .cc-sort-btn:hover { background: #f3f4f6; }
746
+ .cc-sort-btn.cc-active { background: #111827; color: #fff; }
747
+
748
+ /* ── Comments List ───────────────────────────────────── */
749
+ .cc-comments-list {
750
+ flex: 1;
751
+ overflow-y: auto;
752
+ overscroll-behavior: contain;
753
+ }
754
+ .cc-comments-list::-webkit-scrollbar { width: 4px; }
755
+ .cc-comments-list::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 4px; }
756
+
757
+ /* ── Comment Card ────────────────────────────────────── */
758
+ .cc-comment-card {
759
+ padding: 16px 20px;
760
+ background: #fff;
761
+ border-bottom: 1px solid #f3f4f6;
762
+ }
763
+ .cc-comment-card.cc-reply {
764
+ padding-left: 36px;
765
+ background: #fafafa;
766
+ }
767
+
768
+ .cc-comment-header {
769
+ display: flex;
770
+ align-items: flex-start;
771
+ justify-content: space-between;
772
+ margin-bottom: 8px;
773
+ }
774
+ .cc-comment-author-row { display: flex; align-items: center; gap: 10px; }
775
+
776
+ .cc-avatar {
777
+ width: 32px; height: 32px;
778
+ border-radius: 50%;
779
+ flex-shrink: 0;
780
+ display: flex;
781
+ align-items: center;
782
+ justify-content: center;
783
+ color: #fff;
784
+ font-weight: 700;
785
+ font-size: 12px;
786
+ overflow: hidden;
787
+ }
788
+ .cc-avatar img { width: 100%; height: 100%; object-fit: cover; }
789
+
790
+ .cc-author-name {
791
+ font-size: 14px;
792
+ font-weight: 700;
793
+ color: #111827;
794
+ }
795
+ .cc-comment-time {
796
+ font-size: 11px;
797
+ color: #9ca3af;
798
+ margin-top: 2px;
799
+ }
800
+
801
+ .cc-comment-body {
802
+ font-size: 14px;
803
+ color: #374151;
804
+ line-height: 1.6;
805
+ margin-bottom: 12px;
806
+ word-break: break-word;
807
+ white-space: pre-wrap;
808
+ }
809
+
810
+ .cc-comment-actions {
811
+ display: flex;
812
+ align-items: center;
813
+ gap: 12px;
814
+ }
815
+ .cc-action-btn {
816
+ display: inline-flex;
817
+ align-items: center;
818
+ gap: 5px;
819
+ background: transparent;
820
+ border: none;
821
+ cursor: pointer;
822
+ font-size: 12px;
823
+ font-weight: 500;
824
+ color: #6b7280;
825
+ padding: 3px 6px;
826
+ border-radius: 6px;
827
+ font-family: ${fontFamily};
828
+ transition: background 0.15s, color 0.15s;
829
+ }
830
+ .cc-action-btn:hover { background: #f3f4f6; }
831
+ .cc-action-btn.cc-liked { color: #ef4444; }
832
+ .cc-action-btn.cc-danger:hover { color: #ef4444; background: #fef2f2; }
833
+ .cc-action-btn.cc-owner-actions { margin-left: auto; }
834
+
835
+ /* ── Reply Subthread ─────────────────────────────────── */
836
+ .cc-subthread-label {
837
+ font-size: 11px;
838
+ font-weight: 600;
839
+ color: #94a3b8;
840
+ text-transform: uppercase;
841
+ letter-spacing: 0.05em;
842
+ padding: 12px 20px 8px;
843
+ background: #f9fafb;
844
+ }
845
+
846
+ /* ── Empty / Loading / Error ─────────────────────────── */
847
+ .cc-empty-state, .cc-loading-state, .cc-error-state {
848
+ text-align: center;
849
+ padding: 60px 20px;
850
+ color: #9ca3af;
851
+ }
852
+ .cc-empty-state p, .cc-loading-state p, .cc-error-state p {
853
+ font-size: 14px;
854
+ font-weight: 500;
855
+ color: #9ca3af;
856
+ margin-bottom: 6px;
857
+ }
858
+ .cc-empty-state span, .cc-loading-state span, .cc-error-state span {
859
+ font-size: 13px;
860
+ color: #d1d5db;
861
+ }
862
+ .cc-error-state .cc-retry-btn {
863
+ margin-top: 16px;
864
+ padding: 8px 16px;
865
+ background: #111827;
866
+ color: #fff;
867
+ border: none;
868
+ border-radius: 6px;
869
+ font-size: 13px;
870
+ font-weight: 500;
871
+ cursor: pointer;
872
+ font-family: ${fontFamily};
873
+ }
874
+ .cc-error-icon { color: #ef4444; margin-bottom: 12px; }
875
+
876
+ .cc-spinner-lg {
877
+ width: 28px; height: 28px;
878
+ border: 2px solid #e5e7eb;
879
+ border-top-color: ${primaryColor};
880
+ border-radius: 50%;
881
+ animation: cc-spin 0.7s linear infinite;
882
+ margin: 0 auto 12px;
883
+ }
884
+ @keyframes cc-spin { to { transform: rotate(360deg); } }
885
+
886
+ /* ── Compose Box ─────────────────────────────────────── */
887
+ .cc-compose {
888
+ border-top: 1px solid #e5e7eb;
889
+ padding: 12px 20px;
890
+ background: #fff;
891
+ flex-shrink: 0;
892
+ position: relative;
893
+ }
894
+
895
+ .cc-compose-textarea {
896
+ width: 100%;
897
+ min-height: 72px;
898
+ max-height: 180px;
899
+ border: 1.5px solid #d1d5db;
900
+ border-radius: 8px;
901
+ padding: 10px 12px;
902
+ font-size: 14px;
903
+ font-family: ${fontFamily};
904
+ color: #111827;
905
+ resize: vertical;
906
+ outline: none;
907
+ background: #fff;
908
+ transition: border-color 0.15s;
909
+ line-height: 1.5;
910
+ }
911
+ .cc-compose-textarea:focus { border-color: ${primaryColor}; }
912
+ .cc-compose-textarea::placeholder { color: #9ca3af; }
913
+
914
+ .cc-compose-actions {
915
+ display: flex;
916
+ justify-content: space-between;
917
+ align-items: center;
918
+ margin-top: 8px;
919
+ gap: 8px;
920
+ }
921
+ .cc-compose-cancel {
922
+ background: transparent;
923
+ border: none;
924
+ cursor: pointer;
925
+ font-size: 13px;
926
+ color: #6b7280;
927
+ font-family: ${fontFamily};
928
+ padding: 6px;
929
+ display: none;
930
+ }
931
+ .cc-compose-cancel.cc-visible { display: block; }
932
+ .cc-compose-submit {
933
+ display: inline-flex;
934
+ align-items: center;
935
+ gap: 6px;
936
+ height: 36px;
937
+ padding: 0 16px;
938
+ background: ${primaryColor};
939
+ color: #fff;
940
+ border: none;
941
+ border-radius: 7px;
942
+ font-size: 13px;
943
+ font-weight: 600;
944
+ cursor: pointer;
945
+ font-family: ${fontFamily};
946
+ margin-left: auto;
947
+ transition: opacity 0.15s;
948
+ }
949
+ .cc-compose-submit:disabled { opacity: 0.55; cursor: not-allowed; }
950
+
951
+ .cc-spinner-sm {
952
+ width: 14px; height: 14px;
953
+ border: 2px solid rgba(255,255,255,0.4);
954
+ border-top-color: #fff;
955
+ border-radius: 50%;
956
+ animation: cc-spin 0.7s linear infinite;
957
+ }
958
+
959
+ /* ── Login Overlay inside compose ────────────────────── */
960
+ .cc-login-overlay {
961
+ position: absolute;
962
+ inset: 0;
963
+ background: rgba(255,255,255,0.95);
964
+ display: flex;
965
+ flex-direction: column;
966
+ align-items: center;
967
+ justify-content: center;
968
+ gap: 10px;
969
+ padding: 16px;
970
+ border-top: 1px solid #e5e7eb;
971
+ }
972
+ .cc-login-overlay p {
973
+ font-size: 14px;
974
+ color: #6b7280;
975
+ text-align: center;
976
+ }
977
+ .cc-login-overlay-btn {
978
+ height: 40px;
979
+ padding: 0 20px;
980
+ background: ${primaryColor};
981
+ color: #fff;
982
+ border: none;
983
+ border-radius: 8px;
984
+ font-size: 14px;
985
+ font-weight: 600;
986
+ cursor: pointer;
987
+ font-family: ${fontFamily};
988
+ }
989
+ `;
990
+ }
991
+
992
+ /**
993
+ * Safe DOM text node — never use innerHTML with user-generated content.
994
+ * Returns a text node that can be appended to any element.
995
+ */
996
+ /**
997
+ * Set an element's text content safely (no HTML injection).
998
+ */
999
+ function setTextContent(el, str) {
1000
+ el.textContent = str;
1001
+ }
1002
+ /**
1003
+ * Create an element with safe text content.
1004
+ */
1005
+ function el(tag, text, attrs) {
1006
+ const element = document.createElement(tag);
1007
+ if (text !== undefined)
1008
+ element.textContent = text;
1009
+ return element;
1010
+ }
1011
+ /**
1012
+ * Render comment content as safe DOM nodes.
1013
+ * Supports newlines → <br> but no raw HTML from user input.
1014
+ * Returns a DocumentFragment.
1015
+ */
1016
+ function renderCommentContent(raw) {
1017
+ const fragment = document.createDocumentFragment();
1018
+ // Replace \n with a delimiter we can split on
1019
+ const lines = raw.split('\n');
1020
+ lines.forEach((line, i) => {
1021
+ fragment.appendChild(document.createTextNode(line));
1022
+ if (i < lines.length - 1) {
1023
+ fragment.appendChild(document.createElement('br'));
1024
+ }
1025
+ });
1026
+ return fragment;
1027
+ }
1028
+ /**
1029
+ * Validate that a URL is safe (http/https only).
1030
+ * Returns null if unsafe.
1031
+ */
1032
+ function sanitizeUrl(url) {
1033
+ try {
1034
+ const parsed = new URL(url);
1035
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
1036
+ return null;
1037
+ return parsed.toString();
1038
+ }
1039
+ catch (_a) {
1040
+ return null;
1041
+ }
1042
+ }
1043
+
1044
+ const HOST_ID = 'cc-paywall-host';
1045
+ function createPaywallRenderer(config) {
1046
+ let root = null;
1047
+ let overlay = null;
1048
+ function init() {
1049
+ const { root: shadowRoot } = createShadowHost(HOST_ID);
1050
+ root = shadowRoot;
1051
+ injectStyles(root, getPaywallStyles(config.theme.primaryColor, config.theme.fontFamily));
1052
+ overlay = el('div');
1053
+ overlay.className = 'cc-paywall-overlay';
1054
+ root.appendChild(overlay);
1055
+ }
1056
+ function render(state, callbacks, meta) {
1057
+ var _a, _b, _c;
1058
+ if (!overlay)
1059
+ init();
1060
+ if (!overlay)
1061
+ return;
1062
+ // Clear previous content
1063
+ while (overlay.firstChild)
1064
+ overlay.removeChild(overlay.firstChild);
1065
+ switch (state) {
1066
+ case 'checking':
1067
+ renderChecking(overlay);
1068
+ break;
1069
+ case 'login':
1070
+ renderLogin(overlay, callbacks);
1071
+ break;
1072
+ case 'purchase':
1073
+ renderPurchase(overlay, callbacks, (_a = meta === null || meta === void 0 ? void 0 : meta.requiredCredits) !== null && _a !== void 0 ? _a : null);
1074
+ break;
1075
+ case 'insufficient':
1076
+ 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);
1077
+ break;
1078
+ case 'loading':
1079
+ renderLoading(overlay);
1080
+ break;
1081
+ case 'granted':
1082
+ destroy();
1083
+ break;
1084
+ }
1085
+ }
1086
+ function renderChecking(parent) {
1087
+ const spinner = el('div');
1088
+ spinner.className = 'cc-spinner';
1089
+ spinner.style.margin = '0 auto 12px';
1090
+ spinner.style.width = '24px';
1091
+ spinner.style.height = '24px';
1092
+ spinner.style.borderWidth = '2px';
1093
+ spinner.style.borderColor = '#e5e7eb';
1094
+ spinner.style.borderTopColor = config.theme.primaryColor;
1095
+ const text = el('p', 'Checking access...');
1096
+ text.style.cssText = 'font-size:14px;color:#6b7280;text-align:center;font-family:' + config.theme.fontFamily;
1097
+ parent.appendChild(spinner);
1098
+ parent.appendChild(text);
1099
+ }
1100
+ function renderLogin(parent, cb) {
1101
+ parent.appendChild(el('h2', 'This article requires a subscription'));
1102
+ parent.appendChild(el('p', 'Log in with your Content Credits account to unlock this article.'));
1103
+ const btn = el('button', 'Login & Buy with Content Credits');
1104
+ btn.className = 'cc-btn cc-btn-primary';
1105
+ btn.addEventListener('click', cb.onLogin);
1106
+ parent.appendChild(btn);
1107
+ parent.appendChild(poweredBy());
1108
+ }
1109
+ function renderPurchase(parent, cb, credits) {
1110
+ parent.appendChild(el('h2', 'Unlock this article'));
1111
+ if (credits !== null) {
1112
+ const badge = el('span', `${credits} credit${credits !== 1 ? 's' : ''}`);
1113
+ badge.className = 'cc-credit-badge';
1114
+ parent.appendChild(badge);
1115
+ }
1116
+ parent.appendChild(el('p', 'Use your Content Credits balance to instantly access this premium article.'));
1117
+ const btn = el('button', credits !== null ? `Buy for ${credits} Credit${credits !== 1 ? 's' : ''}` : 'Buy with Content Credits');
1118
+ btn.className = 'cc-btn cc-btn-primary';
1119
+ btn.addEventListener('click', cb.onPurchase);
1120
+ parent.appendChild(btn);
1121
+ parent.appendChild(poweredBy());
1122
+ }
1123
+ function renderInsufficient(parent, cb, required, available) {
1124
+ parent.appendChild(el('h2', 'Not enough credits'));
1125
+ const detail = required !== null && available !== null
1126
+ ? `You need ${required} credit${required !== 1 ? 's' : ''} but have ${available}. Top up to unlock this article.`
1127
+ : 'You don\'t have enough credits to unlock this article. Purchase more to continue.';
1128
+ parent.appendChild(el('p', detail));
1129
+ const btn = el('button', 'Buy More Credits');
1130
+ btn.className = 'cc-btn cc-btn-primary';
1131
+ btn.addEventListener('click', cb.onBuyMoreCredits);
1132
+ parent.appendChild(btn);
1133
+ parent.appendChild(poweredBy());
1134
+ }
1135
+ function renderLoading(parent) {
1136
+ const btn = el('button');
1137
+ btn.className = 'cc-btn cc-btn-primary';
1138
+ btn.disabled = true;
1139
+ const spinner = el('span');
1140
+ spinner.className = 'cc-spinner';
1141
+ btn.appendChild(spinner);
1142
+ btn.appendChild(document.createTextNode(' Processing…'));
1143
+ parent.appendChild(btn);
1144
+ }
1145
+ function poweredBy() {
1146
+ const div = el('div');
1147
+ div.className = 'cc-powered-by';
1148
+ div.textContent = 'Powered by ';
1149
+ const link = el('a', 'Content Credits');
1150
+ link.setAttribute('href', 'https://contentcredits.com');
1151
+ link.setAttribute('target', '_blank');
1152
+ link.setAttribute('rel', 'noopener noreferrer');
1153
+ div.appendChild(link);
1154
+ return div;
1155
+ }
1156
+ function setButtonLoading(loading) {
1157
+ if (!overlay)
1158
+ return;
1159
+ const btn = overlay.querySelector('.cc-btn');
1160
+ if (!btn)
1161
+ return;
1162
+ btn.disabled = loading;
1163
+ if (loading) {
1164
+ const spinner = el('span');
1165
+ spinner.className = 'cc-spinner';
1166
+ setTextContent(btn, '');
1167
+ btn.appendChild(spinner);
1168
+ btn.appendChild(document.createTextNode(' Processing…'));
1169
+ }
1170
+ }
1171
+ function destroy() {
1172
+ removeShadowHost(HOST_ID);
1173
+ root = null;
1174
+ overlay = null;
1175
+ }
1176
+ return { init, render, setButtonLoading, destroy };
1177
+ }
1178
+
1179
+ const DETECTION_TIMEOUT_MS = 2000;
1180
+ /**
1181
+ * Detect whether the Content Credits browser extension is installed.
1182
+ *
1183
+ * Strategy 1: Load a known icon from the chrome-extension:// URL.
1184
+ * - Fastest and most reliable for Chrome/Edge.
1185
+ *
1186
+ * Strategy 2: Check a flag the extension sets on window.
1187
+ * - Extension can set `window.__CC_EXTENSION_LOADED = true` in a content script.
1188
+ *
1189
+ * Strategy 3: Timeout fallback — if neither resolves within DETECTION_TIMEOUT_MS,
1190
+ * assume not installed.
1191
+ */
1192
+ function detectExtension(extensionId) {
1193
+ if (!extensionId || typeof extensionId !== 'string') {
1194
+ return Promise.resolve(false);
1195
+ }
1196
+ return new Promise(resolve => {
1197
+ let resolved = false;
1198
+ function done(result) {
1199
+ if (!resolved) {
1200
+ resolved = true;
1201
+ resolve(result);
1202
+ }
1203
+ }
1204
+ // Strategy 2: flag check (instantaneous if extension is loaded)
1205
+ if (window.__CC_EXTENSION_LOADED === true) {
1206
+ done(true);
1207
+ return;
1208
+ }
1209
+ // Strategy 1: image load from chrome-extension:// protocol
1210
+ const img = new Image();
1211
+ img.onload = () => done(true);
1212
+ img.onerror = () => done(false);
1213
+ img.src = `chrome-extension://${extensionId}/icons/icon16.png`;
1214
+ // Strategy 3: timeout safety net
1215
+ setTimeout(() => done(false), DETECTION_TIMEOUT_MS);
1216
+ });
1217
+ }
1218
+
1219
+ /**
1220
+ * The set of origins we will accept postMessage messages from.
1221
+ * Only the accounts frontend and the current page itself (for extension relay).
1222
+ */
1223
+ function getAllowedOrigins() {
1224
+ const origins = ["https://accounts.contentcredits.com"];
1225
+ // Allow current page origin (extension relays messages through the page)
1226
+ try {
1227
+ origins.push(window.location.origin);
1228
+ }
1229
+ catch (_a) {
1230
+ // ignore
1231
+ }
1232
+ return origins;
1233
+ }
1234
+ function createExtensionBridge() {
1235
+ let authHandler = null;
1236
+ let purchaseHandler = null;
1237
+ function handleMessage(event) {
1238
+ var _a, _b, _c, _d;
1239
+ // Security: validate the origin before trusting any message
1240
+ const allowed = getAllowedOrigins();
1241
+ if (!allowed.includes(event.origin) && event.origin !== window.location.origin) {
1242
+ return;
1243
+ }
1244
+ const msg = event.data;
1245
+ if (!msg || typeof msg !== 'object' || !msg.type)
1246
+ return;
1247
+ switch (msg.type) {
1248
+ case 'authorization_response': {
1249
+ const data = ((_a = msg.data) !== null && _a !== void 0 ? _a : (_b = event.detail) === null || _b === void 0 ? void 0 : _b.data);
1250
+ if (data && authHandler)
1251
+ authHandler(data);
1252
+ break;
1253
+ }
1254
+ case 'purchase_response': {
1255
+ const data = ((_c = msg.data) !== null && _c !== void 0 ? _c : (_d = event.detail) === null || _d === void 0 ? void 0 : _d.data);
1256
+ if (data && purchaseHandler)
1257
+ purchaseHandler(data);
1258
+ break;
1259
+ }
1260
+ }
1261
+ }
1262
+ function attach() {
1263
+ window.addEventListener('message', handleMessage);
1264
+ // Extension also dispatches as CustomEvents on window
1265
+ window.addEventListener('authorization_response', (e) => {
1266
+ const detail = e.detail;
1267
+ if ((detail === null || detail === void 0 ? void 0 : detail.data) && authHandler)
1268
+ authHandler(detail.data);
1269
+ });
1270
+ window.addEventListener('purchase_response', (e) => {
1271
+ const detail = e.detail;
1272
+ if ((detail === null || detail === void 0 ? void 0 : detail.data) && purchaseHandler)
1273
+ purchaseHandler(detail.data);
1274
+ });
1275
+ }
1276
+ function detach() {
1277
+ window.removeEventListener('message', handleMessage);
1278
+ }
1279
+ function requestAuthorization(articleId, hostName) {
1280
+ window.postMessage({ type: 'request_authorization', data: { articleId, hostName } }, window.location.origin);
1281
+ }
1282
+ function requestPurchase(params) {
1283
+ window.postMessage({ type: 'request_purchase', data: params }, window.location.origin);
1284
+ }
1285
+ function requestLogin(hostName) {
1286
+ window.postMessage({ type: 'request_login', data: { hostName } }, window.location.origin);
1287
+ }
1288
+ function onAuthorizationResponse(handler) {
1289
+ authHandler = handler;
1290
+ }
1291
+ function onPurchaseResponse(handler) {
1292
+ purchaseHandler = handler;
1293
+ }
1294
+ return {
1295
+ attach,
1296
+ detach,
1297
+ requestAuthorization,
1298
+ requestPurchase,
1299
+ requestLogin,
1300
+ onAuthorizationResponse,
1301
+ onPurchaseResponse,
1302
+ };
1303
+ }
1304
+
1305
+ const POPUP_NAME = 'ccAuthPopup';
1306
+ const POPUP_SPECS = 'scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,width=600,height=650';
1307
+ function centeredSpecs() {
1308
+ const width = 600;
1309
+ const height = 650;
1310
+ const left = Math.round(window.screenX + (window.outerWidth - width) / 2);
1311
+ const top = Math.round(window.screenY + (window.outerHeight - height) / 2);
1312
+ return `${POPUP_SPECS},left=${left},top=${top}`;
1313
+ }
1314
+ function isMobileDevice() {
1315
+ return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
1316
+ || window.innerWidth < 768;
1317
+ }
1318
+ /**
1319
+ * Scrub `token` and `cc_token` query parameters from the current URL
1320
+ * so the token doesn't appear in browser history or server logs.
1321
+ */
1322
+ function scrubTokenFromUrl() {
1323
+ try {
1324
+ const url = new URL(window.location.href);
1325
+ let changed = false;
1326
+ ['token', 'cc_token'].forEach(param => {
1327
+ if (url.searchParams.has(param)) {
1328
+ url.searchParams.delete(param);
1329
+ changed = true;
1330
+ }
1331
+ });
1332
+ if (changed) {
1333
+ history.replaceState(null, '', url.toString());
1334
+ }
1335
+ }
1336
+ catch (_a) {
1337
+ // ignore in environments without history API
1338
+ }
1339
+ }
1340
+ /**
1341
+ * Read and store a token that may have been placed in the current URL
1342
+ * (e.g. after a mobile redirect back from the accounts site).
1343
+ * Always scrubs the token from the URL after reading it.
1344
+ */
1345
+ function consumeTokenFromUrl() {
1346
+ var _a;
1347
+ try {
1348
+ const url = new URL(window.location.href);
1349
+ const token = (_a = url.searchParams.get('token')) !== null && _a !== void 0 ? _a : url.searchParams.get('cc_token');
1350
+ scrubTokenFromUrl();
1351
+ if (token) {
1352
+ tokenStorage.set(token);
1353
+ return token;
1354
+ }
1355
+ }
1356
+ catch (_b) {
1357
+ // ignore
1358
+ }
1359
+ return null;
1360
+ }
1361
+ /**
1362
+ * Open a centered auth popup and poll for the token callback.
1363
+ * Returns a promise that resolves with the token when login completes,
1364
+ * or null if the popup is closed without completing login.
1365
+ */
1366
+ function openAuthPopup(authUrl) {
1367
+ return new Promise(resolve => {
1368
+ let popup = null;
1369
+ try {
1370
+ popup = window.open(authUrl, POPUP_NAME, centeredSpecs());
1371
+ }
1372
+ catch (_a) {
1373
+ // popup blocked — fall through to null
1374
+ }
1375
+ // Popup blocked
1376
+ if (!popup || popup.closed) {
1377
+ resolve(null);
1378
+ return;
1379
+ }
1380
+ const POLL_MS = 200;
1381
+ const MAX_WAIT_MS = 5 * 60 * 1000; // 5 minutes
1382
+ let elapsed = 0;
1383
+ const timer = setInterval(() => {
1384
+ var _a;
1385
+ elapsed += POLL_MS;
1386
+ if (!popup || popup.closed) {
1387
+ clearInterval(timer);
1388
+ resolve(tokenStorage.get()); // may have been set just before close
1389
+ return;
1390
+ }
1391
+ if (elapsed > MAX_WAIT_MS) {
1392
+ clearInterval(timer);
1393
+ try {
1394
+ popup.close();
1395
+ }
1396
+ catch ( /* ignore */_b) { /* ignore */ }
1397
+ resolve(null);
1398
+ return;
1399
+ }
1400
+ try {
1401
+ // Only readable once popup navigates back to our origin
1402
+ const popupUrl = popup.location.href;
1403
+ if (popupUrl.includes('/auth/callback') || popupUrl.includes('cc_token=') || popupUrl.includes('token=')) {
1404
+ const params = new URLSearchParams(popup.location.search);
1405
+ const token = (_a = params.get('token')) !== null && _a !== void 0 ? _a : params.get('cc_token');
1406
+ if (token) {
1407
+ tokenStorage.set(token);
1408
+ clearInterval(timer);
1409
+ try {
1410
+ popup.close();
1411
+ }
1412
+ catch ( /* ignore */_c) { /* ignore */ }
1413
+ resolve(token);
1414
+ }
1415
+ }
1416
+ }
1417
+ catch (_d) {
1418
+ // cross-origin error — popup is on accounts domain, not ours yet; keep polling
1419
+ }
1420
+ }, POLL_MS);
1421
+ });
1422
+ }
1423
+
1424
+ function createPaywall(config, creditsApi, state, emitter) {
1425
+ const gate = createGate({
1426
+ selector: config.contentSelector,
1427
+ teaserParagraphs: config.teaserParagraphs,
1428
+ });
1429
+ const renderer = createPaywallRenderer(config);
1430
+ const bridge = createExtensionBridge();
1431
+ let extensionAvailable = false;
1432
+ // ── Helpers ──────────────────────────────────────────────────────────────
1433
+ function buildAuthUrl() {
1434
+ const redirect = encodeURIComponent(config.articleUrl);
1435
+ return `${"https://accounts.contentcredits.com"}/authenticate/extension?redirect=${redirect}`;
1436
+ }
1437
+ function handleAccessGranted(creditsSpent = 0, balance = 0) {
1438
+ var _a;
1439
+ state.set({ hasAccess: true, isLoaded: true, isLoading: false });
1440
+ gate.reveal();
1441
+ renderer.render('granted', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1442
+ emitter.emit('paywall:hidden', {});
1443
+ emitter.emit('article:purchased', { creditsSpent, remainingBalance: balance });
1444
+ (_a = config.onAccessGranted) === null || _a === void 0 ? void 0 : _a.call(config);
1445
+ }
1446
+ // ── Login ─────────────────────────────────────────────────────────────────
1447
+ async function doLogin() {
1448
+ const authUrl = buildAuthUrl();
1449
+ if (extensionAvailable) {
1450
+ bridge.requestLogin(config.hostName);
1451
+ return;
1452
+ }
1453
+ if (isMobileDevice()) {
1454
+ // Full-page redirect — popup is blocked on mobile
1455
+ window.location.href = authUrl;
1456
+ return;
1457
+ }
1458
+ renderer.render('loading', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1459
+ const token = await openAuthPopup(authUrl);
1460
+ if (token) {
1461
+ state.set({ isLoggedIn: true });
1462
+ await checkAccess();
1463
+ }
1464
+ else {
1465
+ // Popup closed without login
1466
+ renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1467
+ }
1468
+ }
1469
+ // ── Purchase ──────────────────────────────────────────────────────────────
1470
+ async function doPurchase() {
1471
+ var _a, _b, _c;
1472
+ if (!tokenStorage.has()) {
1473
+ await doLogin();
1474
+ return;
1475
+ }
1476
+ if (extensionAvailable) {
1477
+ bridge.requestPurchase({
1478
+ articleId: config.apiKey,
1479
+ hostName: config.hostName,
1480
+ location: config.articleUrl,
1481
+ title: config.pageTitle,
1482
+ });
1483
+ return;
1484
+ }
1485
+ renderer.render('loading', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1486
+ state.set({ isLoading: true });
1487
+ try {
1488
+ const result = await creditsApi.purchaseArticle({
1489
+ apiKey: config.apiKey,
1490
+ postUrl: config.articleUrl,
1491
+ postName: config.pageTitle,
1492
+ hostName: config.hostName,
1493
+ });
1494
+ if (result.success) {
1495
+ handleAccessGranted(0, 0);
1496
+ }
1497
+ else {
1498
+ state.set({ isLoading: false });
1499
+ renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1500
+ emitter.emit('error', { message: (_a = result.message) !== null && _a !== void 0 ? _a : 'Purchase failed' });
1501
+ }
1502
+ }
1503
+ catch (err) {
1504
+ state.set({ isLoading: false });
1505
+ if (err instanceof ApiError && err.status === 402) {
1506
+ // Insufficient credits
1507
+ renderer.render('insufficient', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits }, {
1508
+ requiredCredits: state.get().requiredCredits,
1509
+ creditBalance: state.get().creditBalance,
1510
+ });
1511
+ emitter.emit('credits:insufficient', {
1512
+ required: (_b = state.get().requiredCredits) !== null && _b !== void 0 ? _b : 0,
1513
+ available: (_c = state.get().creditBalance) !== null && _c !== void 0 ? _c : 0,
1514
+ });
1515
+ }
1516
+ else {
1517
+ renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1518
+ emitter.emit('error', { message: 'Purchase failed', error: err });
1519
+ }
1520
+ }
1521
+ }
1522
+ function doBuyMoreCredits() {
1523
+ window.open(`${"https://accounts.contentcredits.com"}/consumer/dashboard`, '_blank', 'noopener,noreferrer');
1524
+ }
1525
+ // ── Access Check ──────────────────────────────────────────────────────────
1526
+ async function checkAccess() {
1527
+ state.set({ isLoading: true });
1528
+ renderer.render('checking', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1529
+ if (extensionAvailable) {
1530
+ bridge.requestAuthorization(config.apiKey, config.hostName);
1531
+ return; // response handled in onAuthorizationResponse
1532
+ }
1533
+ if (!tokenStorage.has()) {
1534
+ state.set({ isLoading: false, isLoaded: true });
1535
+ gate.hide();
1536
+ renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1537
+ emitter.emit('paywall:shown', {});
1538
+ return;
1539
+ }
1540
+ try {
1541
+ const result = await creditsApi.checkAccess({
1542
+ apiKey: config.apiKey,
1543
+ postUrl: config.articleUrl,
1544
+ postName: config.pageTitle,
1545
+ hostName: config.hostName,
1546
+ });
1547
+ state.set({
1548
+ isLoading: false,
1549
+ isLoaded: true,
1550
+ hasAccess: result.success,
1551
+ });
1552
+ if (result.success) {
1553
+ handleAccessGranted(0, 0);
1554
+ }
1555
+ else {
1556
+ gate.hide();
1557
+ renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1558
+ emitter.emit('paywall:shown', {});
1559
+ }
1560
+ }
1561
+ catch (err) {
1562
+ state.set({ isLoading: false, isLoaded: true });
1563
+ gate.hide();
1564
+ renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1565
+ if (!(err instanceof ApiError && err.status === 401)) {
1566
+ emitter.emit('error', { message: 'Access check failed', error: err });
1567
+ }
1568
+ }
1569
+ }
1570
+ // ── Init ──────────────────────────────────────────────────────────────────
1571
+ async function init() {
1572
+ // Check if user just returned from a mobile login redirect
1573
+ const redirectToken = consumeTokenFromUrl();
1574
+ if (redirectToken) {
1575
+ state.set({ isLoggedIn: true });
1576
+ }
1577
+ // Detect extension
1578
+ extensionAvailable = await detectExtension(config.extensionId);
1579
+ state.set({ isExtensionAvailable: extensionAvailable });
1580
+ if (extensionAvailable) {
1581
+ bridge.attach();
1582
+ bridge.onAuthorizationResponse(data => {
1583
+ var _a, _b, _c;
1584
+ state.set({
1585
+ isLoggedIn: data.isAuthenticated,
1586
+ hasAccess: data.doesHaveAccess,
1587
+ isLoaded: true,
1588
+ isLoading: false,
1589
+ creditBalance: (_a = data.creditBalance) !== null && _a !== void 0 ? _a : null,
1590
+ requiredCredits: (_b = data.requiredCredits) !== null && _b !== void 0 ? _b : null,
1591
+ });
1592
+ if (!data.isAuthenticated) {
1593
+ gate.hide();
1594
+ renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1595
+ emitter.emit('paywall:shown', {});
1596
+ }
1597
+ else if (data.doesHaveAccess) {
1598
+ handleAccessGranted(0, (_c = data.creditBalance) !== null && _c !== void 0 ? _c : 0);
1599
+ }
1600
+ else {
1601
+ gate.hide();
1602
+ renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits }, {
1603
+ requiredCredits: data.requiredCredits,
1604
+ creditBalance: data.creditBalance,
1605
+ });
1606
+ emitter.emit('paywall:shown', {});
1607
+ }
1608
+ });
1609
+ bridge.onPurchaseResponse(data => {
1610
+ var _a, _b;
1611
+ state.set({ isLoading: false, isLoaded: true, hasAccess: data.doesHaveAccess });
1612
+ if (data.doesHaveAccess) {
1613
+ handleAccessGranted((_a = data.creditsSpent) !== null && _a !== void 0 ? _a : 0, (_b = data.creditBalance) !== null && _b !== void 0 ? _b : 0);
1614
+ }
1615
+ else {
1616
+ renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
1617
+ emitter.emit('error', { message: 'Purchase failed via extension' });
1618
+ }
1619
+ });
1620
+ }
1621
+ await checkAccess();
1622
+ }
1623
+ function destroy() {
1624
+ bridge.detach();
1625
+ renderer.destroy();
1626
+ gate.reveal();
1627
+ }
1628
+ return { init, checkAccess, destroy };
1629
+ }
1630
+
1631
+ const POSITION_KEY = 'cc-widget-pos';
1632
+ const WIDGET_HEIGHT = 60;
1633
+ function createCommentWidget(primaryColor, onOpen) {
1634
+ let widget = null;
1635
+ let badgeEl = null;
1636
+ function mount() {
1637
+ if (document.getElementById('cc-comment-widget'))
1638
+ return;
1639
+ // Restore saved vertical position
1640
+ let topPercent = 50;
1641
+ try {
1642
+ const saved = localStorage.getItem(POSITION_KEY);
1643
+ if (saved)
1644
+ topPercent = JSON.parse(saved);
1645
+ }
1646
+ catch ( /* ignore */_a) { /* ignore */ }
1647
+ widget = document.createElement('div');
1648
+ widget.id = 'cc-comment-widget';
1649
+ // Inline styles so no external stylesheet dependency and no shadow DOM needed
1650
+ // (widget is a minimal host-page element, panel uses shadow DOM)
1651
+ widget.style.cssText = `
1652
+ position:fixed;top:${topPercent}%;right:0;transform:translateY(-50%);
1653
+ height:${WIDGET_HEIGHT}px;background:${primaryColor};border-radius:10px 0 0 10px;
1654
+ display:flex;align-items:center;gap:8px;padding:0 8px 0 12px;
1655
+ z-index:2147483646;box-shadow:-2px 0 16px rgba(0,0,0,.12);
1656
+ cursor:pointer;user-select:none;
1657
+ font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
1658
+ transition:filter .2s;
1659
+ `;
1660
+ widget.setAttribute('role', 'button');
1661
+ widget.setAttribute('aria-label', 'Open comments');
1662
+ widget.tabIndex = 0;
1663
+ // Chat icon
1664
+ const icon = document.createElement('div');
1665
+ icon.style.cssText = 'color:#fff;display:flex;align-items:center;flex-shrink:0;';
1666
+ icon.innerHTML = `<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`;
1667
+ // Comment count badge
1668
+ badgeEl = document.createElement('span');
1669
+ badgeEl.style.cssText = `
1670
+ background:#fff;color:${primaryColor};border-radius:12px;
1671
+ padding:2px 7px;font-size:12px;font-weight:700;min-width:20px;
1672
+ text-align:center;display:none;
1673
+ `;
1674
+ // Drag handle
1675
+ const handle = document.createElement('div');
1676
+ handle.style.cssText = 'color:rgba(255,255,255,.65);cursor:grab;display:flex;align-items:center;padding:0 2px;flex-shrink:0;';
1677
+ handle.innerHTML = `<svg width="8" height="22" viewBox="0 0 8 22" fill="none"><circle cx="4" cy="4" r="2" fill="currentColor"/><circle cx="4" cy="11" r="2" fill="currentColor"/><circle cx="4" cy="18" r="2" fill="currentColor"/></svg>`;
1678
+ // Click to open panel
1679
+ icon.addEventListener('click', onOpen);
1680
+ badgeEl.addEventListener('click', onOpen);
1681
+ // Drag to reposition
1682
+ let dragging = false;
1683
+ let startY = 0;
1684
+ let startTop = 0;
1685
+ function beginDrag(y) {
1686
+ dragging = true;
1687
+ startY = y;
1688
+ startTop = widget.getBoundingClientRect().top;
1689
+ handle.style.cursor = 'grabbing';
1690
+ }
1691
+ function moveDrag(y) {
1692
+ if (!dragging || !widget)
1693
+ return;
1694
+ const delta = y - startY;
1695
+ const newTop = Math.max(0, Math.min(window.innerHeight - WIDGET_HEIGHT, startTop + delta));
1696
+ const pct = (newTop / window.innerHeight) * 100;
1697
+ widget.style.top = `${pct}%`;
1698
+ }
1699
+ function endDrag() {
1700
+ if (!dragging || !widget)
1701
+ return;
1702
+ dragging = false;
1703
+ handle.style.cursor = 'grab';
1704
+ const rect = widget.getBoundingClientRect();
1705
+ const pct = (rect.top / window.innerHeight) * 100;
1706
+ try {
1707
+ localStorage.setItem(POSITION_KEY, JSON.stringify(pct));
1708
+ }
1709
+ catch ( /* ignore */_a) { /* ignore */ }
1710
+ }
1711
+ handle.addEventListener('mousedown', e => { e.preventDefault(); e.stopPropagation(); beginDrag(e.clientY); });
1712
+ handle.addEventListener('touchstart', e => { e.preventDefault(); beginDrag(e.touches[0].clientY); }, { passive: false });
1713
+ document.addEventListener('mousemove', e => moveDrag(e.clientY));
1714
+ document.addEventListener('touchmove', e => moveDrag(e.touches[0].clientY), { passive: true });
1715
+ document.addEventListener('mouseup', endDrag);
1716
+ document.addEventListener('touchend', endDrag);
1717
+ widget.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ')
1718
+ onOpen(); });
1719
+ widget.appendChild(icon);
1720
+ widget.appendChild(badgeEl);
1721
+ widget.appendChild(handle);
1722
+ document.body.appendChild(widget);
1723
+ }
1724
+ function setCount(n) {
1725
+ if (!badgeEl)
1726
+ return;
1727
+ if (n > 0) {
1728
+ badgeEl.textContent = n > 99 ? '99+' : String(n);
1729
+ badgeEl.style.display = 'inline-block';
1730
+ }
1731
+ else {
1732
+ badgeEl.style.display = 'none';
1733
+ }
1734
+ }
1735
+ function show() {
1736
+ if (widget)
1737
+ widget.style.display = 'flex';
1738
+ }
1739
+ function hide() {
1740
+ if (widget)
1741
+ widget.style.display = 'none';
1742
+ }
1743
+ function destroy() {
1744
+ widget === null || widget === void 0 ? void 0 : widget.remove();
1745
+ widget = null;
1746
+ badgeEl = null;
1747
+ }
1748
+ return { mount, setCount, show, hide, destroy };
1749
+ }
1750
+
1751
+ const PANEL_HOST_ID = 'cc-comments-host';
1752
+ const AVATAR_COLORS = ['#6366f1', '#ec4899', '#8b5cf6', '#14b8a6', '#f59e0b', '#ef4444'];
1753
+ function avatarColor(name) {
1754
+ return AVATAR_COLORS[(name || 'A').charCodeAt(0) % AVATAR_COLORS.length];
1755
+ }
1756
+ function initials(first, last) {
1757
+ var _a, _b, _c, _d;
1758
+ return `${(_b = (_a = first === null || first === void 0 ? void 0 : first[0]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : ''}${(_d = (_c = last === null || last === void 0 ? void 0 : last[0]) === null || _c === void 0 ? void 0 : _c.toUpperCase()) !== null && _d !== void 0 ? _d : ''}` || '?';
1759
+ }
1760
+ function formatDate(iso) {
1761
+ try {
1762
+ const d = new Date(iso);
1763
+ const time = d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
1764
+ const day = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
1765
+ return `${time} · ${day}`;
1766
+ }
1767
+ catch (_a) {
1768
+ return '';
1769
+ }
1770
+ }
1771
+ function createCommentPanel(config, commentsApi, emitter, onClose) {
1772
+ let root = null;
1773
+ let currentUserId = null;
1774
+ let currentThreadId = null;
1775
+ let currentComments = [];
1776
+ let currentSort = 'TOP';
1777
+ let viewingSubthreadFor = null;
1778
+ let replyingToCommentId = null;
1779
+ let editingCommentId = null;
1780
+ // ── Shadow DOM setup ──────────────────────────────────────────────────────
1781
+ function ensureRoot() {
1782
+ if (root)
1783
+ return root;
1784
+ const { root: r } = createShadowHost(PANEL_HOST_ID);
1785
+ root = r;
1786
+ injectStyles(root, getCommentStyles(config.theme.primaryColor, config.theme.fontFamily));
1787
+ return root;
1788
+ }
1789
+ // ── Auth ──────────────────────────────────────────────────────────────────
1790
+ function refreshUser() {
1791
+ const token = tokenStorage.get();
1792
+ if (token) {
1793
+ currentUserId = getUserIdFromToken(token);
1794
+ }
1795
+ else {
1796
+ currentUserId = null;
1797
+ }
1798
+ }
1799
+ async function doLogin() {
1800
+ const redirectUrl = encodeURIComponent(config.articleUrl);
1801
+ const authUrl = `${"https://accounts.contentcredits.com"}/authenticate/extension?redirect=${redirectUrl}`;
1802
+ if (isMobileDevice()) {
1803
+ window.location.href = authUrl;
1804
+ return;
1805
+ }
1806
+ const token = await openAuthPopup(authUrl);
1807
+ if (token) {
1808
+ refreshUser();
1809
+ updateLoginOverlay();
1810
+ void loadComments();
1811
+ }
1812
+ }
1813
+ // ── Thread & Comments Loading ─────────────────────────────────────────────
1814
+ async function loadComments() {
1815
+ var _a;
1816
+ const r = ensureRoot();
1817
+ const listEl = r.getElementById('cc-comments-list');
1818
+ if (!listEl)
1819
+ return;
1820
+ renderLoading(listEl);
1821
+ try {
1822
+ // 1. Ensure a thread exists for this page (backend returns thread object directly)
1823
+ const threadRes = await commentsApi.ensureThread({
1824
+ pageUrl: config.articleUrl,
1825
+ hostname: config.hostName,
1826
+ });
1827
+ if (!threadRes._id) {
1828
+ renderError(listEl, 'Comments are not available for this page.');
1829
+ return;
1830
+ }
1831
+ currentThreadId = threadRes._id;
1832
+ // 2. Fetch comments (backend returns { thread, comments } — no success wrapper)
1833
+ const commentsRes = await commentsApi.getComments({
1834
+ pageUrl: config.articleUrl,
1835
+ sortBy: currentSort,
1836
+ });
1837
+ currentComments = (_a = commentsRes.comments) !== null && _a !== void 0 ? _a : [];
1838
+ if (commentsRes.thread)
1839
+ currentThreadId = commentsRes.thread._id;
1840
+ // Update count badge in header
1841
+ const countEl = r.getElementById('cc-header-count');
1842
+ if (countEl)
1843
+ setTextContent(countEl, String(currentComments.length));
1844
+ renderComments(listEl);
1845
+ }
1846
+ catch (_b) {
1847
+ renderError(listEl, 'Unable to reach the server. Check your connection.');
1848
+ }
1849
+ }
1850
+ // ── Comment Tree Building ─────────────────────────────────────────────────
1851
+ function buildTree(comments) {
1852
+ const map = new Map();
1853
+ const roots = [];
1854
+ comments.forEach(c => map.set(c._id, Object.assign(Object.assign({}, c), { replies: [] })));
1855
+ comments.forEach(c => {
1856
+ const node = map.get(c._id);
1857
+ if (c.parentCommentId) {
1858
+ const parent = map.get(c.parentCommentId);
1859
+ if (parent && !parent.parentCommentId)
1860
+ parent.replies.push(node);
1861
+ }
1862
+ else {
1863
+ roots.push(node);
1864
+ }
1865
+ });
1866
+ return roots;
1867
+ }
1868
+ function sortTree(roots) {
1869
+ const sorted = [...roots];
1870
+ if (currentSort === 'NEWEST') {
1871
+ sorted.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
1872
+ }
1873
+ else {
1874
+ // TOP — sort by reply count then likes
1875
+ sorted.sort((a, b) => {
1876
+ var _a, _b, _c, _d, _e, _f;
1877
+ return (((_b = (_a = b.replies) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) - ((_d = (_c = a.replies) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0)) ||
1878
+ (((_e = b.likeCount) !== null && _e !== void 0 ? _e : 0) - ((_f = a.likeCount) !== null && _f !== void 0 ? _f : 0));
1879
+ });
1880
+ }
1881
+ sorted.forEach(c => {
1882
+ var _a;
1883
+ if ((_a = c.replies) === null || _a === void 0 ? void 0 : _a.length) {
1884
+ c.replies.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
1885
+ }
1886
+ });
1887
+ return sorted;
1888
+ }
1889
+ // ── DOM Rendering ─────────────────────────────────────────────────────────
1890
+ function renderLoading(container) {
1891
+ container.innerHTML = '';
1892
+ const div = el('div');
1893
+ div.className = 'cc-loading-state';
1894
+ const spinner = el('div');
1895
+ spinner.className = 'cc-spinner-lg';
1896
+ div.appendChild(spinner);
1897
+ div.appendChild(el('p', 'Loading comments…'));
1898
+ container.appendChild(div);
1899
+ }
1900
+ function renderError(container, message) {
1901
+ container.innerHTML = '';
1902
+ const div = el('div');
1903
+ div.className = 'cc-error-state';
1904
+ const icon = el('div');
1905
+ icon.className = 'cc-error-icon';
1906
+ icon.innerHTML = `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`;
1907
+ div.appendChild(icon);
1908
+ div.appendChild(el('p', message));
1909
+ const retry = el('button', 'Try Again');
1910
+ retry.className = 'cc-retry-btn';
1911
+ retry.addEventListener('click', () => void loadComments());
1912
+ div.appendChild(retry);
1913
+ container.appendChild(div);
1914
+ }
1915
+ function renderComments(container) {
1916
+ container.innerHTML = '';
1917
+ const r = ensureRoot();
1918
+ // Update back button and title
1919
+ const backBtn = r.getElementById('cc-back-btn');
1920
+ const titleEl = r.getElementById('cc-panel-title');
1921
+ if (viewingSubthreadFor) {
1922
+ backBtn === null || backBtn === void 0 ? void 0 : backBtn.classList.add('cc-visible');
1923
+ if (titleEl)
1924
+ setTextContent(titleEl, 'Replies');
1925
+ }
1926
+ else {
1927
+ backBtn === null || backBtn === void 0 ? void 0 : backBtn.classList.remove('cc-visible');
1928
+ if (titleEl)
1929
+ setTextContent(titleEl, 'Comments');
1930
+ }
1931
+ if (viewingSubthreadFor) {
1932
+ renderSubthread(container, viewingSubthreadFor);
1933
+ return;
1934
+ }
1935
+ if (currentComments.length === 0) {
1936
+ const empty = el('div');
1937
+ empty.className = 'cc-empty-state';
1938
+ empty.appendChild(el('p', 'No comments yet'));
1939
+ const sub = el('span', 'Be the first to share your thoughts');
1940
+ empty.appendChild(sub);
1941
+ container.appendChild(empty);
1942
+ return;
1943
+ }
1944
+ const tree = sortTree(buildTree(currentComments));
1945
+ tree.forEach(c => container.appendChild(buildCommentEl(c, false)));
1946
+ }
1947
+ function renderSubthread(container, parentId) {
1948
+ var _a, _b, _c, _d, _e;
1949
+ const tree = buildTree(currentComments);
1950
+ const parent = tree.find(c => c._id === parentId);
1951
+ if (!parent)
1952
+ return;
1953
+ container.appendChild(buildCommentEl(parent, false));
1954
+ const label = el('div', `${(_b = (_a = parent.replies) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0} ${((_d = (_c = parent.replies) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0) === 1 ? 'REPLY' : 'REPLIES'}`);
1955
+ label.className = 'cc-subthread-label';
1956
+ container.appendChild(label);
1957
+ if (!((_e = parent.replies) === null || _e === void 0 ? void 0 : _e.length)) {
1958
+ const empty = el('div');
1959
+ empty.className = 'cc-empty-state';
1960
+ empty.style.paddingTop = '20px';
1961
+ empty.appendChild(el('p', 'No replies yet'));
1962
+ container.appendChild(empty);
1963
+ }
1964
+ else {
1965
+ parent.replies.forEach(r => container.appendChild(buildCommentEl(r, true)));
1966
+ }
1967
+ }
1968
+ function buildCommentEl(comment, isReply) {
1969
+ var _a, _b;
1970
+ const isOwn = !!(currentUserId && comment.authorId === currentUserId);
1971
+ const author = comment.author;
1972
+ const authorName = author ? `${author.firstName} ${author.lastName}`.trim() : 'Anonymous';
1973
+ const avatarBg = avatarColor(authorName);
1974
+ const inis = author ? initials(author.firstName, author.lastName) : '?';
1975
+ const card = el('div');
1976
+ card.className = `cc-comment-card${isReply ? ' cc-reply' : ''}`;
1977
+ card.dataset.commentId = comment._id;
1978
+ // Header row
1979
+ const header = el('div');
1980
+ header.className = 'cc-comment-header';
1981
+ const authorRow = el('div');
1982
+ authorRow.className = 'cc-comment-author-row';
1983
+ // Avatar
1984
+ const avatar = el('div');
1985
+ avatar.className = 'cc-avatar';
1986
+ avatar.style.background = avatarBg;
1987
+ if (author === null || author === void 0 ? void 0 : author.profilePicture) {
1988
+ const safeUrl = sanitizeUrl(author.profilePicture.startsWith('http')
1989
+ ? author.profilePicture
1990
+ : `${config.apiBaseUrl}${author.profilePicture}`);
1991
+ if (safeUrl) {
1992
+ const img = el('img');
1993
+ img.setAttribute('src', safeUrl);
1994
+ img.setAttribute('alt', authorName);
1995
+ img.addEventListener('error', () => {
1996
+ img.remove();
1997
+ setTextContent(avatar, inis);
1998
+ });
1999
+ avatar.appendChild(img);
2000
+ }
2001
+ else {
2002
+ setTextContent(avatar, inis);
2003
+ }
2004
+ }
2005
+ else {
2006
+ setTextContent(avatar, inis);
2007
+ }
2008
+ const authorMeta = el('div');
2009
+ const nameEl = el('div', authorName);
2010
+ nameEl.className = 'cc-author-name';
2011
+ const timeEl = el('div', formatDate(comment.createdAt));
2012
+ timeEl.className = 'cc-comment-time';
2013
+ authorMeta.appendChild(nameEl);
2014
+ authorMeta.appendChild(timeEl);
2015
+ authorRow.appendChild(avatar);
2016
+ authorRow.appendChild(authorMeta);
2017
+ header.appendChild(authorRow);
2018
+ card.appendChild(header);
2019
+ // Body — safe DOM construction, never innerHTML of user content
2020
+ const body = el('div');
2021
+ body.className = 'cc-comment-body';
2022
+ body.appendChild(renderCommentContent(comment.content));
2023
+ card.appendChild(body);
2024
+ // Actions row
2025
+ const actions = el('div');
2026
+ actions.className = 'cc-comment-actions';
2027
+ // Reply button (only on top-level)
2028
+ if (!isReply) {
2029
+ const replyBtn = el('button');
2030
+ replyBtn.className = 'cc-action-btn';
2031
+ replyBtn.dataset.commentId = comment._id;
2032
+ replyBtn.dataset.action = 'reply';
2033
+ replyBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="transform:scaleX(-1)"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`;
2034
+ replyBtn.appendChild(document.createTextNode(` ${((_a = comment.replies) === null || _a === void 0 ? void 0 : _a.length) || 'Reply'}`));
2035
+ actions.appendChild(replyBtn);
2036
+ }
2037
+ // Like button
2038
+ const likeBtn = el('button');
2039
+ likeBtn.className = `cc-action-btn${comment.hasLiked ? ' cc-liked' : ''}`;
2040
+ likeBtn.dataset.commentId = comment._id;
2041
+ likeBtn.dataset.action = 'like';
2042
+ likeBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="${comment.hasLiked ? 'currentColor' : 'none'}" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>`;
2043
+ likeBtn.appendChild(document.createTextNode(` ${(_b = comment.likeCount) !== null && _b !== void 0 ? _b : 0}`));
2044
+ actions.appendChild(likeBtn);
2045
+ // Owner controls
2046
+ if (isOwn) {
2047
+ const ownerDiv = el('div');
2048
+ ownerDiv.className = 'cc-action-btn cc-owner-actions';
2049
+ ownerDiv.style.cssText = 'margin-left:auto;display:flex;gap:4px;background:transparent;border:none;padding:0;';
2050
+ const editBtn = el('button', 'Edit');
2051
+ editBtn.className = 'cc-action-btn';
2052
+ editBtn.dataset.commentId = comment._id;
2053
+ editBtn.dataset.action = 'edit';
2054
+ const deleteBtn = el('button', 'Delete');
2055
+ deleteBtn.className = 'cc-action-btn cc-danger';
2056
+ deleteBtn.dataset.commentId = comment._id;
2057
+ deleteBtn.dataset.action = 'delete';
2058
+ ownerDiv.appendChild(editBtn);
2059
+ ownerDiv.appendChild(deleteBtn);
2060
+ actions.appendChild(ownerDiv);
2061
+ }
2062
+ card.appendChild(actions);
2063
+ return card;
2064
+ }
2065
+ // ── Panel DOM Structure ───────────────────────────────────────────────────
2066
+ function buildPanel() {
2067
+ const panel = el('div');
2068
+ panel.className = 'cc-panel';
2069
+ panel.id = 'cc-comments-panel';
2070
+ // Header
2071
+ const header = el('div');
2072
+ header.className = 'cc-panel-header';
2073
+ const backBtn = el('button');
2074
+ backBtn.className = 'cc-back-btn';
2075
+ backBtn.id = 'cc-back-btn';
2076
+ backBtn.setAttribute('aria-label', 'Back');
2077
+ backBtn.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>`;
2078
+ backBtn.addEventListener('click', () => {
2079
+ viewingSubthreadFor = null;
2080
+ replyingToCommentId = null;
2081
+ const listEl = root === null || root === void 0 ? void 0 : root.getElementById('cc-comments-list');
2082
+ if (listEl)
2083
+ renderComments(listEl);
2084
+ });
2085
+ const titleGroup = el('div');
2086
+ titleGroup.style.cssText = 'flex:1;display:flex;align-items:center;gap:6px;';
2087
+ const titleEl = el('span', 'Comments');
2088
+ titleEl.className = 'cc-panel-title';
2089
+ titleEl.id = 'cc-panel-title';
2090
+ const countEl = el('span', '');
2091
+ countEl.className = 'cc-panel-count';
2092
+ countEl.id = 'cc-header-count';
2093
+ titleGroup.appendChild(titleEl);
2094
+ titleGroup.appendChild(countEl);
2095
+ const closeBtn = el('button');
2096
+ closeBtn.className = 'cc-panel-close-btn';
2097
+ closeBtn.setAttribute('aria-label', 'Close comments');
2098
+ closeBtn.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
2099
+ closeBtn.addEventListener('click', closePanel);
2100
+ header.appendChild(backBtn);
2101
+ header.appendChild(titleGroup);
2102
+ header.appendChild(closeBtn);
2103
+ panel.appendChild(header);
2104
+ // Sort bar
2105
+ const sortBar = el('div');
2106
+ sortBar.className = 'cc-sort-bar';
2107
+ const sortLabel = el('span', 'Sort:');
2108
+ sortLabel.className = 'cc-sort-label';
2109
+ sortBar.appendChild(sortLabel);
2110
+ ['TOP', 'NEWEST'].forEach(sort => {
2111
+ const btn = el('button', sort);
2112
+ btn.className = `cc-sort-btn${currentSort === sort ? ' cc-active' : ''}`;
2113
+ btn.dataset.sort = sort;
2114
+ btn.addEventListener('click', () => {
2115
+ if (currentSort === sort)
2116
+ return;
2117
+ currentSort = sort;
2118
+ sortBar.querySelectorAll('.cc-sort-btn').forEach(b => b.classList.remove('cc-active'));
2119
+ btn.classList.add('cc-active');
2120
+ void loadComments();
2121
+ });
2122
+ sortBar.appendChild(btn);
2123
+ });
2124
+ panel.appendChild(sortBar);
2125
+ // Comments list
2126
+ const list = el('div');
2127
+ list.className = 'cc-comments-list';
2128
+ list.id = 'cc-comments-list';
2129
+ list.addEventListener('click', handleListClick);
2130
+ panel.appendChild(list);
2131
+ // Compose area
2132
+ panel.appendChild(buildCompose());
2133
+ return panel;
2134
+ }
2135
+ function buildCompose() {
2136
+ const compose = el('div');
2137
+ compose.className = 'cc-compose';
2138
+ compose.id = 'cc-compose';
2139
+ const textarea = el('textarea');
2140
+ textarea.className = 'cc-compose-textarea';
2141
+ textarea.id = 'cc-compose-textarea';
2142
+ textarea.setAttribute('placeholder', 'Write a comment…');
2143
+ textarea.setAttribute('rows', '3');
2144
+ compose.appendChild(textarea);
2145
+ const actions = el('div');
2146
+ actions.className = 'cc-compose-actions';
2147
+ const cancelBtn = el('button', 'Cancel');
2148
+ cancelBtn.className = 'cc-compose-cancel';
2149
+ cancelBtn.id = 'cc-compose-cancel';
2150
+ cancelBtn.addEventListener('click', cancelEdit);
2151
+ actions.appendChild(cancelBtn);
2152
+ const submitBtn = el('button', 'Post');
2153
+ submitBtn.className = 'cc-compose-submit';
2154
+ submitBtn.id = 'cc-compose-submit';
2155
+ submitBtn.addEventListener('click', () => void handleSubmit());
2156
+ actions.appendChild(submitBtn);
2157
+ compose.appendChild(actions);
2158
+ // Login overlay inside compose
2159
+ if (!tokenStorage.has()) {
2160
+ compose.appendChild(buildLoginOverlay());
2161
+ }
2162
+ return compose;
2163
+ }
2164
+ function buildLoginOverlay() {
2165
+ const overlay = el('div');
2166
+ overlay.className = 'cc-login-overlay';
2167
+ overlay.id = 'cc-login-overlay';
2168
+ overlay.appendChild(el('p', 'Sign in to join the conversation'));
2169
+ const btn = el('button', 'Login with Content Credits');
2170
+ btn.className = 'cc-login-overlay-btn';
2171
+ btn.addEventListener('click', () => void doLogin());
2172
+ overlay.appendChild(btn);
2173
+ return overlay;
2174
+ }
2175
+ // ── Interactions ──────────────────────────────────────────────────────────
2176
+ function handleListClick(e) {
2177
+ const target = e.target;
2178
+ const btn = target.closest('[data-action]');
2179
+ if (!btn)
2180
+ return;
2181
+ const action = btn.dataset.action;
2182
+ const commentId = btn.dataset.commentId;
2183
+ if (!commentId)
2184
+ return;
2185
+ switch (action) {
2186
+ case 'reply':
2187
+ handleReply(commentId);
2188
+ break;
2189
+ case 'like':
2190
+ void handleLike(commentId);
2191
+ break;
2192
+ case 'edit':
2193
+ handleEdit(commentId);
2194
+ break;
2195
+ case 'delete':
2196
+ void handleDelete(commentId);
2197
+ break;
2198
+ }
2199
+ }
2200
+ function handleReply(commentId) {
2201
+ if (!viewingSubthreadFor) {
2202
+ // First click on Reply → drill into the subthread view
2203
+ viewingSubthreadFor = commentId;
2204
+ const listEl = root === null || root === void 0 ? void 0 : root.getElementById('cc-comments-list');
2205
+ if (listEl)
2206
+ renderComments(listEl);
2207
+ }
2208
+ else {
2209
+ // Already in subthread → set the reply target and focus textarea
2210
+ replyingToCommentId = commentId;
2211
+ editingCommentId = null;
2212
+ const textarea = root === null || root === void 0 ? void 0 : root.getElementById('cc-compose-textarea');
2213
+ if (textarea) {
2214
+ textarea.placeholder = 'Write a reply…';
2215
+ textarea.focus();
2216
+ }
2217
+ showCancel();
2218
+ }
2219
+ }
2220
+ async function handleLike(commentId) {
2221
+ var _a, _b, _c, _d, _e;
2222
+ if (!tokenStorage.has()) {
2223
+ void doLogin();
2224
+ return;
2225
+ }
2226
+ // Optimistic update
2227
+ const comment = currentComments.find(c => c._id === commentId);
2228
+ if (!comment)
2229
+ return;
2230
+ const wasLiked = comment.hasLiked;
2231
+ comment.hasLiked = !wasLiked;
2232
+ comment.likeCount = ((_a = comment.likeCount) !== null && _a !== void 0 ? _a : 0) + (comment.hasLiked ? 1 : -1);
2233
+ const listEl = root === null || root === void 0 ? void 0 : root.getElementById('cc-comments-list');
2234
+ if (listEl)
2235
+ renderComments(listEl);
2236
+ try {
2237
+ // Backend returns { success: true, data: { _id, likeCount, hasLiked } }
2238
+ const res = await commentsApi.toggleLike(commentId);
2239
+ if (res.success) {
2240
+ if (typeof ((_b = res.data) === null || _b === void 0 ? void 0 : _b.hasLiked) === 'boolean')
2241
+ comment.hasLiked = res.data.hasLiked;
2242
+ if (typeof ((_c = res.data) === null || _c === void 0 ? void 0 : _c.likeCount) === 'number')
2243
+ comment.likeCount = res.data.likeCount;
2244
+ emitter.emit('comment:liked', { commentId, hasLiked: comment.hasLiked });
2245
+ }
2246
+ else {
2247
+ // Rollback
2248
+ comment.hasLiked = wasLiked;
2249
+ comment.likeCount = ((_d = comment.likeCount) !== null && _d !== void 0 ? _d : 0) + (wasLiked ? 1 : -1);
2250
+ }
2251
+ if (listEl)
2252
+ renderComments(listEl);
2253
+ }
2254
+ catch (_f) {
2255
+ comment.hasLiked = wasLiked;
2256
+ comment.likeCount = ((_e = comment.likeCount) !== null && _e !== void 0 ? _e : 0) + (wasLiked ? 1 : -1);
2257
+ if (listEl)
2258
+ renderComments(listEl);
2259
+ }
2260
+ }
2261
+ function handleEdit(commentId) {
2262
+ const comment = currentComments.find(c => c._id === commentId);
2263
+ if (!comment)
2264
+ return;
2265
+ editingCommentId = commentId;
2266
+ replyingToCommentId = null;
2267
+ const textarea = root === null || root === void 0 ? void 0 : root.getElementById('cc-compose-textarea');
2268
+ if (textarea) {
2269
+ textarea.value = comment.content;
2270
+ textarea.placeholder = 'Edit your comment…';
2271
+ textarea.focus();
2272
+ }
2273
+ const submitBtn = root === null || root === void 0 ? void 0 : root.getElementById('cc-compose-submit');
2274
+ if (submitBtn)
2275
+ setTextContent(submitBtn, 'Update');
2276
+ showCancel();
2277
+ }
2278
+ async function handleDelete(commentId) {
2279
+ if (!confirm('Delete this comment?'))
2280
+ return;
2281
+ try {
2282
+ // Backend returns the deleted comment object directly (check _id for success)
2283
+ const res = await commentsApi.deleteComment(commentId);
2284
+ if (res._id) {
2285
+ emitter.emit('comment:deleted', { commentId });
2286
+ void loadComments();
2287
+ }
2288
+ }
2289
+ catch ( /* ignore */_a) { /* ignore */ }
2290
+ }
2291
+ async function handleSubmit() {
2292
+ const textarea = root === null || root === void 0 ? void 0 : root.getElementById('cc-compose-textarea');
2293
+ if (!textarea)
2294
+ return;
2295
+ const content = textarea.value.trim();
2296
+ if (!content)
2297
+ return;
2298
+ if (!tokenStorage.has()) {
2299
+ void doLogin();
2300
+ return;
2301
+ }
2302
+ if (!currentThreadId)
2303
+ return;
2304
+ const submitBtn = root === null || root === void 0 ? void 0 : root.getElementById('cc-compose-submit');
2305
+ if (submitBtn) {
2306
+ submitBtn.disabled = true;
2307
+ setTextContent(submitBtn, 'Posting…');
2308
+ }
2309
+ try {
2310
+ // Both editComment and postComment return the comment object directly
2311
+ let res;
2312
+ if (editingCommentId) {
2313
+ res = await commentsApi.editComment(editingCommentId, content);
2314
+ }
2315
+ else {
2316
+ res = await commentsApi.postComment({
2317
+ threadId: currentThreadId,
2318
+ content,
2319
+ parentCommentId: replyingToCommentId !== null && replyingToCommentId !== void 0 ? replyingToCommentId : viewingSubthreadFor,
2320
+ });
2321
+ emitter.emit('comment:posted', { comment: res });
2322
+ }
2323
+ if (res._id) {
2324
+ textarea.value = '';
2325
+ editingCommentId = null;
2326
+ replyingToCommentId = null;
2327
+ textarea.placeholder = viewingSubthreadFor ? 'Write a reply…' : 'Write a comment…';
2328
+ hideCancel();
2329
+ if (submitBtn)
2330
+ setTextContent(submitBtn, 'Post');
2331
+ void loadComments();
2332
+ }
2333
+ }
2334
+ catch ( /* handled by loadComments */_a) { /* handled by loadComments */ }
2335
+ finally {
2336
+ if (submitBtn) {
2337
+ submitBtn.disabled = false;
2338
+ }
2339
+ }
2340
+ }
2341
+ function showCancel() {
2342
+ const cancelBtn = root === null || root === void 0 ? void 0 : root.getElementById('cc-compose-cancel');
2343
+ cancelBtn === null || cancelBtn === void 0 ? void 0 : cancelBtn.classList.add('cc-visible');
2344
+ }
2345
+ function hideCancel() {
2346
+ const cancelBtn = root === null || root === void 0 ? void 0 : root.getElementById('cc-compose-cancel');
2347
+ cancelBtn === null || cancelBtn === void 0 ? void 0 : cancelBtn.classList.remove('cc-visible');
2348
+ }
2349
+ function cancelEdit() {
2350
+ editingCommentId = null;
2351
+ replyingToCommentId = null;
2352
+ const textarea = root === null || root === void 0 ? void 0 : root.getElementById('cc-compose-textarea');
2353
+ if (textarea) {
2354
+ textarea.value = '';
2355
+ textarea.placeholder = 'Write a comment…';
2356
+ }
2357
+ const submitBtn = root === null || root === void 0 ? void 0 : root.getElementById('cc-compose-submit');
2358
+ if (submitBtn)
2359
+ setTextContent(submitBtn, 'Post');
2360
+ hideCancel();
2361
+ }
2362
+ function updateLoginOverlay() {
2363
+ const r = ensureRoot();
2364
+ const existing = r.getElementById('cc-login-overlay');
2365
+ if (tokenStorage.has() && existing) {
2366
+ existing.remove();
2367
+ }
2368
+ else if (!tokenStorage.has() && !existing) {
2369
+ const compose = r.getElementById('cc-compose');
2370
+ compose === null || compose === void 0 ? void 0 : compose.appendChild(buildLoginOverlay());
2371
+ }
2372
+ }
2373
+ // ── Open / Close ──────────────────────────────────────────────────────────
2374
+ function openPanel() {
2375
+ const r = ensureRoot();
2376
+ // Check if token arrived from redirect
2377
+ const redirectToken = consumeTokenFromUrl();
2378
+ if (redirectToken)
2379
+ refreshUser();
2380
+ refreshUser();
2381
+ // Build panel if not already present
2382
+ let backdrop = r.getElementById('cc-panel-backdrop');
2383
+ if (!backdrop) {
2384
+ backdrop = el('div');
2385
+ backdrop.className = 'cc-panel-backdrop';
2386
+ backdrop.id = 'cc-panel-backdrop';
2387
+ backdrop.addEventListener('click', closePanel);
2388
+ r.appendChild(backdrop);
2389
+ r.appendChild(buildPanel());
2390
+ }
2391
+ // Animate open
2392
+ requestAnimationFrame(() => {
2393
+ var _a;
2394
+ backdrop.classList.add('cc-visible');
2395
+ (_a = r.getElementById('cc-comments-panel')) === null || _a === void 0 ? void 0 : _a.classList.add('cc-open');
2396
+ });
2397
+ updateLoginOverlay();
2398
+ void loadComments();
2399
+ }
2400
+ function closePanel() {
2401
+ const r = root;
2402
+ if (!r)
2403
+ return;
2404
+ const backdrop = r.getElementById('cc-panel-backdrop');
2405
+ const panel = r.getElementById('cc-comments-panel');
2406
+ backdrop === null || backdrop === void 0 ? void 0 : backdrop.classList.remove('cc-visible');
2407
+ panel === null || panel === void 0 ? void 0 : panel.classList.remove('cc-open');
2408
+ setTimeout(() => {
2409
+ backdrop === null || backdrop === void 0 ? void 0 : backdrop.remove();
2410
+ panel === null || panel === void 0 ? void 0 : panel.remove();
2411
+ onClose();
2412
+ }, 280);
2413
+ }
2414
+ function destroy() {
2415
+ removeShadowHost(PANEL_HOST_ID);
2416
+ root = null;
2417
+ }
2418
+ return { openPanel, closePanel, destroy };
2419
+ }
2420
+
2421
+ function createComments(config, commentsApi, emitter) {
2422
+ const panel = createCommentPanel(config, commentsApi, emitter, () => widget.show());
2423
+ const widget = createCommentWidget(config.theme.primaryColor, () => {
2424
+ widget.hide();
2425
+ panel.openPanel();
2426
+ });
2427
+ function init() {
2428
+ widget.mount();
2429
+ }
2430
+ function open() {
2431
+ widget.hide();
2432
+ panel.openPanel();
2433
+ }
2434
+ function close() {
2435
+ panel.closePanel();
2436
+ }
2437
+ function destroy() {
2438
+ panel.destroy();
2439
+ widget.destroy();
2440
+ }
2441
+ return { init, open, close, destroy };
2442
+ }
2443
+
2444
+ /**
2445
+ * Content Credits JS SDK v2
2446
+ *
2447
+ * Drop-in paywall and comments for any website.
2448
+ *
2449
+ * CDN (script tag):
2450
+ * <script src="https://cdn.contentcredits.com/sdk/v2/content-credits.umd.min.js"></script>
2451
+ * <script>
2452
+ * ContentCreditsSDK.init({ apiKey: 'YOUR_API_KEY', contentSelector: '#article-body' });
2453
+ * </script>
2454
+ *
2455
+ * npm:
2456
+ * import { ContentCredits } from '@contentcredits/sdk';
2457
+ * ContentCredits.init({ apiKey: 'YOUR_API_KEY', contentSelector: '#article-body' });
2458
+ */
2459
+ class ContentCredits {
2460
+ constructor(config) {
2461
+ this.config = config;
2462
+ this.state = createState();
2463
+ this.emitter = createEventEmitter();
2464
+ this.paywallModule = null;
2465
+ this.commentsModule = null;
2466
+ this.client = createApiClient(config.apiBaseUrl, this.emitter);
2467
+ this.creditsApi = createCreditsApi(this.client);
2468
+ this.commentsApi = createCommentsApi(this.client);
2469
+ }
2470
+ // ── Factory ───────────────────────────────────────────────────────────────
2471
+ /**
2472
+ * Initialise the SDK and immediately start the access check.
2473
+ *
2474
+ * @example
2475
+ * const cc = ContentCredits.init({
2476
+ * apiKey: 'pub_abc123',
2477
+ * contentSelector: '#premium-content',
2478
+ * });
2479
+ */
2480
+ static init(rawConfig) {
2481
+ const config = resolveConfig(rawConfig);
2482
+ const instance = new ContentCredits(config);
2483
+ void instance._start();
2484
+ return instance;
2485
+ }
2486
+ // ── Internal start ────────────────────────────────────────────────────────
2487
+ async _start() {
2488
+ // Handle token that may have arrived in the URL (mobile redirect flow)
2489
+ consumeTokenFromUrl();
2490
+ this.paywallModule = createPaywall(this.config, this.creditsApi, this.state, this.emitter);
2491
+ if (this.config.enableComments) {
2492
+ this.commentsModule = createComments(this.config, this.commentsApi, this.emitter);
2493
+ this.commentsModule.init();
2494
+ }
2495
+ await this.paywallModule.init();
2496
+ this.emitter.emit('ready', { state: this.state.get() });
2497
+ }
2498
+ // ── Public API ────────────────────────────────────────────────────────────
2499
+ /** Subscribe to an SDK event. Returns an unsubscribe function. */
2500
+ on(event, handler) {
2501
+ return this.emitter.on(event, handler);
2502
+ }
2503
+ /** Unsubscribe from an SDK event. */
2504
+ off(event, handler) {
2505
+ this.emitter.off(event, handler);
2506
+ }
2507
+ /** Get a snapshot of the current SDK state. */
2508
+ getState() {
2509
+ return this.state.get();
2510
+ }
2511
+ /** Programmatically trigger an article access check. */
2512
+ async checkAccess() {
2513
+ var _a;
2514
+ await ((_a = this.paywallModule) === null || _a === void 0 ? void 0 : _a.checkAccess());
2515
+ }
2516
+ /** Open the comment panel programmatically. */
2517
+ openComments() {
2518
+ var _a;
2519
+ (_a = this.commentsModule) === null || _a === void 0 ? void 0 : _a.open();
2520
+ }
2521
+ /** Close the comment panel programmatically. */
2522
+ closeComments() {
2523
+ var _a;
2524
+ (_a = this.commentsModule) === null || _a === void 0 ? void 0 : _a.close();
2525
+ }
2526
+ /** Check if the user is currently authenticated. */
2527
+ isLoggedIn() {
2528
+ return tokenStorage.has();
2529
+ }
2530
+ /** Tear down the SDK — removes all UI, event listeners, and stored state. */
2531
+ destroy() {
2532
+ var _a, _b;
2533
+ (_a = this.paywallModule) === null || _a === void 0 ? void 0 : _a.destroy();
2534
+ (_b = this.commentsModule) === null || _b === void 0 ? void 0 : _b.destroy();
2535
+ this.emitter.removeAll();
2536
+ this.state.reset();
2537
+ }
2538
+ /** SDK version string. */
2539
+ static get version() {
2540
+ return "2.0.0";
2541
+ }
2542
+ }
2543
+ // ── Auto-init from script data attributes (CDN usage) ────────────────────────
2544
+ // Partners can include the script like:
2545
+ // <script src="..." data-api-key="pub_abc" data-content-selector="#body"></script>
2546
+ // and the SDK will auto-initialise without any additional JS.
2547
+ function autoInit() {
2548
+ var _a, _b, _c;
2549
+ const script = (_a = document.currentScript) !== null && _a !== void 0 ? _a : document.querySelector('script[data-cc-api-key], script[data-api-key]');
2550
+ if (!script)
2551
+ return;
2552
+ const ds = script.dataset;
2553
+ const apiKey = (_b = ds.ccApiKey) !== null && _b !== void 0 ? _b : ds.apiKey;
2554
+ if (!apiKey)
2555
+ return;
2556
+ const rawConfig = {
2557
+ apiKey,
2558
+ contentSelector: (_c = ds.ccContentSelector) !== null && _c !== void 0 ? _c : ds.contentSelector,
2559
+ teaserParagraphs: ds.ccTeaserParagraphs ? parseInt(ds.ccTeaserParagraphs, 10) : undefined,
2560
+ enableComments: ds.ccEnableComments !== 'false',
2561
+ extensionId: ds.ccExtensionId,
2562
+ debug: ds.ccDebug === 'true',
2563
+ };
2564
+ if (document.readyState === 'loading') {
2565
+ document.addEventListener('DOMContentLoaded', () => ContentCredits.init(rawConfig));
2566
+ }
2567
+ else {
2568
+ ContentCredits.init(rawConfig);
2569
+ }
2570
+ }
2571
+ autoInit();
2572
+
2573
+ exports.ContentCredits = ContentCredits;
2574
+ //# sourceMappingURL=content-credits.cjs.js.map