@contentcredits/sdk 2.12.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -0
- package/README.md +138 -8
- package/dist/api/client.d.ts +0 -0
- package/dist/api/comments.d.ts +0 -0
- package/dist/api/credits.d.ts +0 -0
- package/dist/auth/popup.d.ts +0 -0
- package/dist/auth/session.d.ts +0 -0
- package/dist/auth/storage.d.ts +0 -0
- package/dist/auth/token.d.ts +0 -0
- package/dist/comments/index.d.ts +0 -0
- package/dist/comments/panel.d.ts +0 -0
- package/dist/comments/widget.d.ts +0 -0
- package/dist/content-credits.cjs.js +39 -19
- package/dist/content-credits.cjs.js.map +1 -1
- package/dist/content-credits.d.ts +0 -0
- package/dist/content-credits.esm.js +39 -19
- package/dist/content-credits.esm.js.map +1 -1
- package/dist/content-credits.umd.min.js +1 -1
- package/dist/content-credits.umd.min.js.map +1 -1
- package/dist/core/config.d.ts +0 -0
- package/dist/core/events.d.ts +0 -0
- package/dist/core/state.d.ts +0 -0
- package/dist/extension/bridge.d.ts +0 -0
- package/dist/extension/detector.d.ts +0 -0
- package/dist/index.d.ts +0 -0
- package/dist/paywall/gate.d.ts +0 -0
- package/dist/paywall/index.d.ts +0 -0
- package/dist/paywall/renderer.d.ts +0 -0
- package/dist/types/index.d.ts +0 -0
- package/dist/ui/sanitize.d.ts +0 -0
- package/dist/ui/shadow.d.ts +0 -0
- package/dist/ui/styles.d.ts +0 -0
- package/package.json +1 -1
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -13,9 +13,10 @@ Drop-in paywall and threaded comment system for any website. Add credit-based ar
|
|
|
13
13
|
## What it does
|
|
14
14
|
|
|
15
15
|
- **Paywall** — hides premium content behind a credit gate using a CSS selector. Reveals it instantly when the reader pays. No server-side content splitting needed.
|
|
16
|
+
- **Customisable top slot** — inject your own content (React widget, donation banner, structured items) above the SDK's unlock button.
|
|
16
17
|
- **Comments** — threaded comment panel with likes, edit, delete, and sorting. Rendered in a Shadow DOM so it never conflicts with your CSS.
|
|
17
18
|
- **Auth** — popup-based login on desktop, redirect flow on mobile. Tokens stored in memory (never cookies).
|
|
18
|
-
- **Extension support** — detects the Content Credits Chrome extension for a one-click experience.
|
|
19
|
+
- **Extension support** — detects the Content Credits Chrome extension for a one-click experience, with automatic fallback if the extension service worker is unresponsive.
|
|
19
20
|
|
|
20
21
|
---
|
|
21
22
|
|
|
@@ -28,7 +29,7 @@ npm install @contentcredits/sdk
|
|
|
28
29
|
Or via CDN (no build step):
|
|
29
30
|
|
|
30
31
|
```html
|
|
31
|
-
<script src="https://cdn.jsdelivr.net/npm/@contentcredits/sdk@2.
|
|
32
|
+
<script src="https://cdn.jsdelivr.net/npm/@contentcredits/sdk@2.12.0/dist/content-credits.umd.min.js"></script>
|
|
32
33
|
```
|
|
33
34
|
|
|
34
35
|
---
|
|
@@ -44,7 +45,7 @@ Or via CDN (no build step):
|
|
|
44
45
|
</div>
|
|
45
46
|
|
|
46
47
|
<!-- Load and initialise the SDK -->
|
|
47
|
-
<script src="https://cdn.jsdelivr.net/npm/@contentcredits/sdk@2.
|
|
48
|
+
<script src="https://cdn.jsdelivr.net/npm/@contentcredits/sdk@2.12.0/dist/content-credits.umd.min.js"></script>
|
|
48
49
|
<script>
|
|
49
50
|
ContentCreditsSDK.ContentCredits.init({
|
|
50
51
|
apiKey: 'pub_YOUR_API_KEY',
|
|
@@ -59,7 +60,7 @@ Or via CDN (no build step):
|
|
|
59
60
|
|
|
60
61
|
```html
|
|
61
62
|
<script
|
|
62
|
-
src="https://cdn.jsdelivr.net/npm/@contentcredits/sdk@2.
|
|
63
|
+
src="https://cdn.jsdelivr.net/npm/@contentcredits/sdk@2.12.0/dist/content-credits.umd.min.js"
|
|
63
64
|
data-api-key="pub_YOUR_API_KEY"
|
|
64
65
|
data-content-selector="#premium-content"
|
|
65
66
|
data-teaser-paragraphs="2"
|
|
@@ -94,13 +95,90 @@ cc.on('paywall:hidden', () => {
|
|
|
94
95
|
| `teaserParagraphs` | `number` | `2` | Paragraphs to show before the paywall |
|
|
95
96
|
| `enableComments` | `boolean` | `true` | Show the comment widget |
|
|
96
97
|
| `articleUrl` | `string` | `location.href` | Canonical URL of the article |
|
|
98
|
+
| `paywallMode` | `'overlay' \| 'inline'` | `'overlay'` | Paywall layout — overlay sits directly below the teaser; inline is the legacy flow-based panel |
|
|
99
|
+
| `paywallTopSlot` | see below | — | Content rendered above the SDK's unlock button — accepts a React element, structured items, `HTMLElement`, or factory function |
|
|
100
|
+
| `reactDOM` | `ReactDOMAdapter` | — | Your `ReactDOM` instance — required when `paywallTopSlot` is a React element |
|
|
97
101
|
| `theme.primaryColor` | `string` | `'#44C678'` | Brand colour for buttons |
|
|
98
102
|
| `theme.fontFamily` | `string` | system UI | Font for all SDK UI |
|
|
103
|
+
| `headless` | `boolean` | `false` | Disable all built-in DOM/UI — manage everything yourself via state and callbacks |
|
|
99
104
|
| `onAccessGranted` | `() => void` | — | Fires when content is unlocked |
|
|
100
105
|
| `debug` | `boolean` | `false` | Verbose console logging |
|
|
101
106
|
|
|
102
107
|
---
|
|
103
108
|
|
|
109
|
+
## Paywall top slot
|
|
110
|
+
|
|
111
|
+
The `paywallTopSlot` lets you inject custom content — a donation widget, promo banner, or anything else — above the SDK's own unlock button. The slot sits inside the SDK's Shadow DOM so your styles are isolated from the host page.
|
|
112
|
+
|
|
113
|
+
### React widget (recommended for React apps)
|
|
114
|
+
|
|
115
|
+
Pass your ReactDOM instance alongside the JSX element. Works with React 18 (`createRoot`) and React 16/17 (`render`).
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
import ReactDOM from 'react-dom/client'; // React 18
|
|
119
|
+
import { ContentCredits } from '@contentcredits/sdk';
|
|
120
|
+
import { DonationWidget } from './DonationWidget';
|
|
121
|
+
|
|
122
|
+
ContentCredits.init({
|
|
123
|
+
apiKey: 'pub_YOUR_API_KEY',
|
|
124
|
+
reactDOM,
|
|
125
|
+
paywallTopSlot: <DonationWidget />,
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Structured items
|
|
130
|
+
|
|
131
|
+
The SDK renders and styles these consistently inside its Shadow DOM:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
ContentCredits.init({
|
|
135
|
+
apiKey: 'pub_YOUR_API_KEY',
|
|
136
|
+
paywallTopSlot: [
|
|
137
|
+
{ type: 'heading', content: 'Donate to access this story.' },
|
|
138
|
+
{ type: 'text', content: 'Donate now for unlimited stories. Cancel anytime.' },
|
|
139
|
+
{ type: 'button', content: 'See Donation Options', variant: 'primary', onClick: () => openDonateFlow() },
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Available item types:
|
|
145
|
+
|
|
146
|
+
| `type` | Description | Extra props |
|
|
147
|
+
|--------|-------------|-------------|
|
|
148
|
+
| `heading` | Large bold heading | `content` |
|
|
149
|
+
| `subheading` | Medium bold text | `content` |
|
|
150
|
+
| `text` | Body copy | `content` |
|
|
151
|
+
| `button` | Styled button | `content`, `variant` (`primary` \| `secondary` \| `outline`), `onClick` |
|
|
152
|
+
| `divider` | Horizontal rule with optional label | `content` |
|
|
153
|
+
|
|
154
|
+
### HTMLElement
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const banner = document.createElement('div');
|
|
158
|
+
banner.innerHTML = '<strong>Support independent journalism</strong>';
|
|
159
|
+
|
|
160
|
+
ContentCredits.init({
|
|
161
|
+
apiKey: 'pub_YOUR_API_KEY',
|
|
162
|
+
paywallTopSlot: banner,
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Factory function
|
|
167
|
+
|
|
168
|
+
Full control — receives the slot container element and mounts whatever you need:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
ContentCredits.init({
|
|
172
|
+
apiKey: 'pub_YOUR_API_KEY',
|
|
173
|
+
paywallTopSlot: (container) => {
|
|
174
|
+
// vanilla JS, Vue, Svelte — anything goes
|
|
175
|
+
container.innerHTML = `<p>Support us to keep reading.</p>`;
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
104
182
|
## Events
|
|
105
183
|
|
|
106
184
|
```ts
|
|
@@ -128,39 +206,91 @@ const cc = ContentCredits.init(config);
|
|
|
128
206
|
|
|
129
207
|
cc.on(event, handler) // subscribe — returns unsubscribe fn
|
|
130
208
|
cc.off(event, handler) // unsubscribe
|
|
209
|
+
cc.subscribe(fn) // reactive state changes — returns unsubscribe fn
|
|
131
210
|
cc.getState() // → SDKState snapshot
|
|
132
211
|
cc.checkAccess() // trigger access check manually
|
|
212
|
+
cc.login() // open login flow programmatically
|
|
213
|
+
cc.purchase() // trigger article purchase
|
|
214
|
+
cc.buyMoreCredits() // open dashboard to top up balance
|
|
133
215
|
cc.openComments() // open comment panel
|
|
134
216
|
cc.closeComments() // close comment panel
|
|
135
217
|
cc.isLoggedIn() // → boolean
|
|
218
|
+
cc.getToken() // → access token string | null
|
|
219
|
+
cc.logout() // revoke session and clear local auth state
|
|
136
220
|
cc.destroy() // tear down SDK, restore hidden content
|
|
137
221
|
|
|
138
|
-
ContentCredits.version // → "2.
|
|
222
|
+
ContentCredits.version // → "2.12.0"
|
|
139
223
|
```
|
|
140
224
|
|
|
141
225
|
---
|
|
142
226
|
|
|
143
227
|
## React / Next.js
|
|
144
228
|
|
|
229
|
+
### Default UI with a React top slot
|
|
230
|
+
|
|
231
|
+
The simplest integration — the SDK handles all paywall rendering; you only supply the top section:
|
|
232
|
+
|
|
145
233
|
```tsx
|
|
146
|
-
'use client';
|
|
234
|
+
'use client';
|
|
147
235
|
|
|
148
236
|
import { useEffect } from 'react';
|
|
237
|
+
import ReactDOM from 'react-dom/client';
|
|
149
238
|
import { ContentCredits } from '@contentcredits/sdk';
|
|
239
|
+
import { DonationWidget } from './DonationWidget';
|
|
150
240
|
|
|
151
|
-
export function PremiumGate({ apiKey
|
|
241
|
+
export function PremiumGate({ apiKey }: { apiKey: string }) {
|
|
152
242
|
useEffect(() => {
|
|
153
243
|
const cc = ContentCredits.init({
|
|
154
244
|
apiKey,
|
|
155
245
|
contentSelector: '#premium-content',
|
|
246
|
+
reactDOM,
|
|
247
|
+
paywallTopSlot: <DonationWidget />,
|
|
248
|
+
});
|
|
249
|
+
return () => cc.destroy();
|
|
250
|
+
}, [apiKey]);
|
|
251
|
+
|
|
252
|
+
return <div id="premium-content">{/* article content */}</div>;
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Headless mode (full custom UI)
|
|
257
|
+
|
|
258
|
+
Use `headless: true` when you want to build the entire paywall UI yourself in React. The SDK manages auth and access checks but renders no DOM of its own.
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
'use client';
|
|
262
|
+
|
|
263
|
+
import { useEffect, useRef, useState } from 'react';
|
|
264
|
+
import { ContentCredits } from '@contentcredits/sdk';
|
|
265
|
+
import type { SDKState } from '@contentcredits/sdk';
|
|
266
|
+
|
|
267
|
+
export function PremiumGate({ apiKey, children }: { apiKey: string; children: React.ReactNode }) {
|
|
268
|
+
const ccRef = useRef<ContentCredits | null>(null);
|
|
269
|
+
const [state, setState] = useState<SDKState | null>(null);
|
|
270
|
+
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
const cc = ContentCredits.init({
|
|
273
|
+
apiKey,
|
|
274
|
+
headless: true,
|
|
275
|
+
onStateChange: setState,
|
|
156
276
|
});
|
|
277
|
+
ccRef.current = cc;
|
|
157
278
|
return () => cc.destroy();
|
|
158
279
|
}, [apiKey]);
|
|
159
280
|
|
|
160
|
-
return
|
|
281
|
+
if (state?.hasAccess) return <>{children}</>;
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<div>
|
|
285
|
+
{/* your teaser content */}
|
|
286
|
+
<button onClick={() => ccRef.current?.purchase()}>Unlock article</button>
|
|
287
|
+
</div>
|
|
288
|
+
);
|
|
161
289
|
}
|
|
162
290
|
```
|
|
163
291
|
|
|
292
|
+
See [`examples/nextjs-blog`](./examples/nextjs-blog) for a full working Next.js 14 (App Router) implementation.
|
|
293
|
+
|
|
164
294
|
---
|
|
165
295
|
|
|
166
296
|
## Requirements
|
package/dist/api/client.d.ts
CHANGED
|
File without changes
|
package/dist/api/comments.d.ts
CHANGED
|
File without changes
|
package/dist/api/credits.d.ts
CHANGED
|
File without changes
|
package/dist/auth/popup.d.ts
CHANGED
|
File without changes
|
package/dist/auth/session.d.ts
CHANGED
|
File without changes
|
package/dist/auth/storage.d.ts
CHANGED
|
File without changes
|
package/dist/auth/token.d.ts
CHANGED
|
File without changes
|
package/dist/comments/index.d.ts
CHANGED
|
File without changes
|
package/dist/comments/panel.d.ts
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -597,7 +597,7 @@ function createShadowHost(id) {
|
|
|
597
597
|
host = document.createElement('div');
|
|
598
598
|
host.id = id;
|
|
599
599
|
// The host element itself is invisible; only its shadow children show
|
|
600
|
-
host.style.cssText = 'position:fixed;
|
|
600
|
+
host.style.cssText = 'position:fixed;bottom:0;left:0;width:100%;height:auto;z-index:2147483647;';
|
|
601
601
|
document.body.appendChild(host);
|
|
602
602
|
}
|
|
603
603
|
const existing = host._ccShadow;
|
|
@@ -676,34 +676,41 @@ function getPaywallStyles(primaryColor, fontFamily) {
|
|
|
676
676
|
|
|
677
677
|
/* ── Overlay paywall panel — full-width white panel below gated content ── */
|
|
678
678
|
.cc-paywall-overlay {
|
|
679
|
+
/* Fixed to the bottom of the viewport — always visible, full width */
|
|
680
|
+
position: fixed;
|
|
681
|
+
bottom: 0;
|
|
682
|
+
left: 0;
|
|
679
683
|
width: 100%;
|
|
680
684
|
background: #fff;
|
|
685
|
+
box-shadow: 0 -8px 40px rgba(0,0,0,0.10);
|
|
686
|
+
border-top: 1px solid #e5e7eb;
|
|
681
687
|
font-family: ${fontFamily};
|
|
682
688
|
}
|
|
683
689
|
|
|
684
|
-
/* Gradient that fades the article into the
|
|
690
|
+
/* Gradient that fades the article into the panel.
|
|
691
|
+
Sits above the panel via position:absolute + negative top. */
|
|
685
692
|
.cc-paywall-overlay-gradient {
|
|
686
|
-
|
|
693
|
+
position: absolute;
|
|
694
|
+
top: -120px;
|
|
695
|
+
left: 0;
|
|
696
|
+
right: 0;
|
|
687
697
|
height: 120px;
|
|
688
698
|
background: linear-gradient(to bottom, transparent 0%, #fff 100%);
|
|
689
|
-
margin-top: -120px;
|
|
690
699
|
pointer-events: none;
|
|
691
|
-
position: relative;
|
|
692
|
-
z-index: 1;
|
|
693
700
|
}
|
|
694
701
|
|
|
695
702
|
/* Top slot — client-supplied content */
|
|
696
703
|
.cc-paywall-overlay-slot {
|
|
697
|
-
padding:
|
|
704
|
+
padding: 20px 24px 0;
|
|
698
705
|
display: flex;
|
|
699
706
|
flex-direction: column;
|
|
700
707
|
align-items: center;
|
|
701
|
-
gap:
|
|
708
|
+
gap: 10px;
|
|
702
709
|
}
|
|
703
710
|
|
|
704
711
|
/* Our SDK's unlock section below the slot */
|
|
705
712
|
.cc-paywall-overlay-body {
|
|
706
|
-
padding:
|
|
713
|
+
padding: 16px 24px 24px;
|
|
707
714
|
display: flex;
|
|
708
715
|
flex-direction: column;
|
|
709
716
|
align-items: center;
|
|
@@ -1285,7 +1292,12 @@ function createPaywallRenderer(config) {
|
|
|
1285
1292
|
const contentEl = document.querySelector(config.contentSelector);
|
|
1286
1293
|
if (!contentEl)
|
|
1287
1294
|
return;
|
|
1288
|
-
|
|
1295
|
+
// Overlay mode: fixed to the bottom of the viewport — attach to body so it
|
|
1296
|
+
// is never constrained by the article's max-width container.
|
|
1297
|
+
// Inline mode: inserted after the content element in document flow.
|
|
1298
|
+
const { root: shadowRoot } = config.paywallMode === 'overlay'
|
|
1299
|
+
? createShadowHost(HOST_ID)
|
|
1300
|
+
: createInlineShadowHost(HOST_ID, contentEl);
|
|
1289
1301
|
root = shadowRoot;
|
|
1290
1302
|
injectStyles(root, getPaywallStyles(config.theme.primaryColor, config.theme.fontFamily));
|
|
1291
1303
|
if (config.paywallMode === 'overlay') {
|
|
@@ -1301,23 +1313,25 @@ function createPaywallRenderer(config) {
|
|
|
1301
1313
|
shadowRoot.appendChild(body);
|
|
1302
1314
|
}
|
|
1303
1315
|
function initOverlay(shadowRoot, contentEl) {
|
|
1304
|
-
var _a
|
|
1316
|
+
var _a;
|
|
1305
1317
|
const panel = el('div');
|
|
1306
1318
|
panel.className = 'cc-paywall-overlay';
|
|
1307
|
-
// Gradient that visually fades the article into the
|
|
1308
|
-
//
|
|
1309
|
-
// gradient blends correctly on sites with non-white backgrounds.
|
|
1319
|
+
// Gradient that visually fades the article into the panel.
|
|
1320
|
+
// Reads the page background so the gradient colour matches non-white sites.
|
|
1310
1321
|
const gradient = el('div');
|
|
1311
1322
|
gradient.className = 'cc-paywall-overlay-gradient';
|
|
1312
|
-
const
|
|
1313
|
-
if (
|
|
1314
|
-
gradient.style.background = `linear-gradient(to bottom, transparent 0%, ${
|
|
1323
|
+
const pageBg = getComputedStyle(document.body).backgroundColor;
|
|
1324
|
+
if (pageBg && pageBg !== 'rgba(0, 0, 0, 0)' && pageBg !== 'transparent') {
|
|
1325
|
+
gradient.style.background = `linear-gradient(to bottom, transparent 0%, ${pageBg} 100%)`;
|
|
1315
1326
|
}
|
|
1316
1327
|
panel.appendChild(gradient);
|
|
1328
|
+
// Add bottom padding to the content element so the fixed panel
|
|
1329
|
+
// doesn't overlap the last readable line of the teaser.
|
|
1330
|
+
contentEl.style.paddingBottom = '200px';
|
|
1317
1331
|
// Top slot — client content
|
|
1318
1332
|
const slot = el('div');
|
|
1319
1333
|
slot.className = 'cc-paywall-overlay-slot';
|
|
1320
|
-
reactRoot = (
|
|
1334
|
+
reactRoot = (_a = mountTopSlot(slot, config.paywallTopSlot, config.reactDOM)) !== null && _a !== void 0 ? _a : null;
|
|
1321
1335
|
panel.appendChild(slot);
|
|
1322
1336
|
// Only render the "or" divider between slot and body when the slot has content
|
|
1323
1337
|
if (config.paywallTopSlot) {
|
|
@@ -1447,6 +1461,12 @@ function createPaywallRenderer(config) {
|
|
|
1447
1461
|
reactRoot === null || reactRoot === void 0 ? void 0 : reactRoot.unmount();
|
|
1448
1462
|
reactRoot = null;
|
|
1449
1463
|
removeShadowHost(HOST_ID);
|
|
1464
|
+
// Remove the padding we added to the content element
|
|
1465
|
+
if (config.paywallMode === 'overlay') {
|
|
1466
|
+
const contentEl = document.querySelector(config.contentSelector);
|
|
1467
|
+
if (contentEl)
|
|
1468
|
+
contentEl.style.paddingBottom = '';
|
|
1469
|
+
}
|
|
1450
1470
|
root = null;
|
|
1451
1471
|
body = null;
|
|
1452
1472
|
}
|
|
@@ -3207,7 +3227,7 @@ class ContentCredits {
|
|
|
3207
3227
|
}
|
|
3208
3228
|
/** SDK version string. */
|
|
3209
3229
|
static get version() {
|
|
3210
|
-
return "2.
|
|
3230
|
+
return "2.14.0";
|
|
3211
3231
|
}
|
|
3212
3232
|
}
|
|
3213
3233
|
// ── Auto-init from script data attributes (CDN usage) ────────────────────────
|