interface User {
_id: string;
firstName: string;
lastName: string;
email: string;
credits: number;
roles: UserRole[];
isVerified: boolean;
isActive: boolean;
linkedPublisherId?: string;
}
type UserRole = 'consumer' | 'publisher' | 'admin';
interface Comment {
_id: string;
threadId: string;
postId?: string | null;
parentCommentId: string | null;
authorId: string;
content: string;
isActive: boolean;
mentions: string[];
likeCount: number;
hasLiked: boolean;
createdAt: string;
updatedAt: string;
author?: CommentAuthor;
replies?: Comment[];
hiddenBy?: string | null;
hiddenReason?: string | null;
moderatedAt?: string | null;
}
interface CommentAuthor {
_id: string;
firstName: string;
lastName: string;
profilePicture?: string;
}
type CommentSortBy = 'TOP' | 'NEWEST' | 'TIPPED_MOST';
/**
* Loosely-typed ReactDOM adapter — matches both React 18 (`createRoot`) and
* React 16/17 (`render`) without pulling React into the SDK's own bundle.
*/
interface ReactDOMAdapter {
/** React 18+ */
createRoot?(container: Element): {
render(node: unknown): void;
unmount(): void;
};
/** React 16/17 */
render?(node: unknown, container: Element, callback?: () => void): void;
}
interface SDKConfig {
/** Publisher API key from the Content Credits admin panel */
apiKey: string;
/** Full URL of the article page. Defaults to window.location.href */
articleUrl?: string;
/** CSS selector for the element containing the premium content to gate */
contentSelector?: string;
/** Number of visible paragraphs before the paywall kicks in. Default: 2 */
teaserParagraphs?: number;
/** Whether to enable the comment widget. Default: true */
enableComments?: boolean;
/**
* Whether to fire the post-discovery beacon (`POST /posts/observe`) on page
* load. Scrapes `og:*` / JSON-LD / `
` metadata and reports it
* alongside the canonical URL — this is also the view event for analytics
* (design doc §5.1, §7.1). Disable if you're handling discovery yourself
* (e.g. via the WordPress plugin's server-side push) or to opt out of
* anonymous view tracking entirely.
*
* Default: `true`
*/
enableBeacon?: boolean;
/** Visual theme options */
theme?: SDKTheme;
/**
* Paywall display mode.
* - `'inline'` — panel sits below the teaser content in the page flow (original behaviour)
* - `'overlay'` — full-width panel that renders below the gated content (default)
*
* Default: `'overlay'`
*/
paywallMode?: 'inline' | 'overlay';
/**
* Your ReactDOM instance. Required when using `renderPaywall`.
* Supports React 18 (`createRoot`) and React 16/17 (`render`).
*
* @example
* import ReactDOM from 'react-dom/client'; // React 18
* ContentCredits.init({ reactDOM, renderPaywall: ({ mountSdkButton }) => });
*/
reactDOM?: ReactDOMAdapter;
/**
* Custom label for the SDK's unlock/purchase button.
* Defaults to `'Unlock · N credits'` (when price is known) or `'Unlock article'`.
*
* @example
* unlockButtonLabel: 'Unlock Just This Story'
*/
unlockButtonLabel?: string;
/**
* Override the default copy shown in the SDK's built-in paywall states.
* All fields are optional — only supply the strings you want to change.
*
* @example
* paywallCopy: {
* loginHeading: 'Read the full story',
* loginDetail: 'Sign in to access this article with your credits.',
* }
*/
paywallCopy?: {
/** Heading shown in the login state. Default: 'This article requires a subscription' */
loginHeading?: string;
/** Detail shown in the login state. Default: 'Sign in to your Content Credits account to unlock this article.' */
loginDetail?: string;
/** Heading shown in the purchase state. Default: 'Unlock this article' */
purchaseHeading?: string;
/** Detail shown in the purchase state. Default: 'Use your Content Credits balance to instantly access this article.' */
purchaseDetail?: string;
/** Heading shown when credits are insufficient. Default: 'Not enough credits' */
insufficientHeading?: string;
};
/**
* Full-control paywall render function. The publisher renders the entire
* modal content and decides where the SDK's action button appears by passing
* `mountSdkButton` as a React ref callback to any container element.
*
* The SDK mounts its state-aware button (sign in / unlock / top up) and the
* "Powered by Content Credits" line inside whichever element receives the ref.
* Requires `reactDOM` to also be set.
*
* @example
* renderPaywall: ({ mountSdkButton }) => (
*
*
Donate to access this story.
*
*
*
* )
*/
renderPaywall?: (props: {
mountSdkButton: (el: HTMLElement | null) => void;
}) => {
type: unknown;
props: unknown;
key?: unknown;
};
/**
* Whether to show the heading and detail text in the SDK's built-in paywall
* states (login, purchase, insufficient). Set to `false` when your layout
* already provides article context (e.g. via `renderPaywall`).
*
* Default: `true`
*/
showHeadings?: boolean;
/** Called when the user is granted access to the article */
onAccessGranted?: () => void;
/**
* Called on every state change. Receives the full state snapshot.
* Use this as the single reactive hook to drive a custom UI instead of
* calling `cc.subscribe()` separately.
*/
onStateChange?: (state: SDKState) => void;
/**
* Called once the SDK has finished its first access check.
* Equivalent to listening for the `ready` event.
*/
onReady?: (state: SDKState) => void;
/**
* Called when the paywall is reached and the user is **not logged in**.
* Render your login UI here and call `cc.login()` from your button.
*/
onLoginRequired?: () => void;
/**
* Called when the user is logged in but has **not yet purchased** this article.
* Render your unlock/purchase UI here and call `cc.purchase()` from your button.
*/
onPurchaseRequired?: (info: {
requiredCredits: number | null;
creditBalance: number | null;
}) => void;
/**
* Called when the user is logged in but their credit balance is **below** the
* article price. Render a top-up UI here and call `cc.buyMoreCredits()`.
*/
onInsufficientCredits?: (info: {
required: number;
available: number;
}) => void;
/**
* Called after a successful article purchase.
* Equivalent to listening for the `article:purchased` event.
*/
onPurchased?: (info: {
creditsSpent: number;
remainingBalance: number;
}) => void;
/**
* Called when a user logs in.
* Equivalent to listening for the `auth:login` event.
*/
onUserLogin?: (user: User) => void;
/**
* Called when the user logs out.
* Equivalent to listening for the `auth:logout` event.
*/
onUserLogout?: () => void;
/**
* Called when any SDK error occurs.
* Equivalent to listening for the `error` event.
*/
onError?: (info: {
message: string;
error?: unknown;
}) => void;
/** Enable verbose debug logging */
debug?: boolean;
/**
* Headless mode — disables all built-in DOM manipulation and UI rendering.
*
* When `true` the SDK will NOT:
* - hide / reveal the premium content element
* - inject the paywall overlay or gradient fade
*
* Instead it exposes reactive state (via `subscribe()`) and action methods
* (`login()`, `purchase()`, `buyMoreCredits()`) so you can build a fully
* custom paywall UI in React, Vue, Svelte, or plain JS.
*
* Default: `false`
*/
headless?: boolean;
}
interface SDKTheme {
/** Primary brand colour used for buttons and accents. Default: '#44C678' */
primaryColor?: string;
/** Font family for all SDK UI elements */
fontFamily?: string;
/**
* Background colour of the modal backdrop/scrim.
* Accepts any valid CSS colour value.
* Default: 'rgba(0, 0, 0, 0.45)'
*/
backdropColor?: string;
/**
* Fill colour for the SDK's own action buttons (Sign in, Unlock, Top up).
* Intentionally separate from `primaryColor` so publishers can brand their
* own slot buttons differently from the Content Credits controls.
* Default: '#44C678' (Content Credits green)
*/
sdkButtonColor?: string;
}
interface SDKState {
isLoading: boolean;
isExtensionAvailable: boolean;
isLoggedIn: boolean;
hasAccess: boolean;
isLoaded: boolean;
user: User | null;
creditBalance: number | null;
requiredCredits: number | null;
}
interface SDKEventMap {
ready: {
state: SDKState;
};
'auth:login': {
user: User;
};
'auth:logout': Record;
'paywall:shown': Record;
'paywall:hidden': Record;
'article:purchased': {
creditsSpent: number;
remainingBalance: number;
};
'credits:insufficient': {
required: number;
available: number;
};
'comment:posted': {
comment: Comment;
};
'comment:liked': {
commentId: string;
hasLiked: boolean;
};
'comment:deleted': {
commentId: string;
};
error: {
message: string;
error?: unknown;
};
}
type SDKEventName = keyof SDKEventMap;
type SDKEventHandler = (payload: SDKEventMap[K]) => void;
/**
* Content Credits JS SDK v2
*
* Drop-in paywall and comments for any website.
*
* CDN (script tag):
*
*
*
* npm:
* import { ContentCredits } from '@contentcredits/sdk';
* ContentCredits.init({ apiKey: 'YOUR_API_KEY', contentSelector: '#article-body' });
*/
declare class ContentCredits {
private readonly config;
private readonly state;
private readonly emitter;
private readonly client;
private readonly creditsApi;
private readonly commentsApi;
private readonly postsApi;
private paywallModule;
private commentsModule;
private constructor();
/**
* Initialise the SDK and immediately start the access check.
*
* @example
* const cc = ContentCredits.init({
* apiKey: 'pub_abc123',
* contentSelector: '#premium-content',
* });
*/
static init(rawConfig: SDKConfig): ContentCredits;
private _start;
/**
* Subscribe to state changes. The callback receives the full state snapshot
* every time any field changes. Returns an unsubscribe function.
*
* Primarily useful in **headless mode** — lets you drive your own UI from
* reactive state without polling `getState()`.
*
* @example
* const unsubscribe = cc.subscribe((state) => {
* if (state.hasAccess) showFullContent();
* else showPaywall(state);
* });
*/
subscribe(fn: (state: SDKState) => void): () => void;
/**
* Trigger the login flow programmatically.
*
* - Desktop: opens a popup window to the Content Credits auth page.
* - Mobile: performs a full-page redirect.
* - Extension: delegates to the browser extension.
*
* Primarily useful in **headless mode** where you render your own "Login"
* button and call this from its `onClick` handler.
*/
login(): Promise;
/**
* Trigger the article purchase flow programmatically.
*
* Deducts the required credits from the user's balance and, on success,
* updates `state.hasAccess` to `true` and emits `article:purchased`.
*
* If the user is not logged in, this automatically opens the login flow
* first, then proceeds with the purchase.
*
* Primarily useful in **headless mode** where you render your own "Unlock"
* button and call this from its `onClick` handler.
*/
purchase(): Promise;
/**
* Open the Content Credits dashboard in a new tab so the user can top up
* their credit balance.
*
* Primarily useful in **headless mode** when `state.creditBalance` is lower
* than `state.requiredCredits`.
*/
buyMoreCredits(): void;
/** Subscribe to an SDK event. Returns an unsubscribe function. */
on(event: K, handler: SDKEventHandler): () => void;
/** Unsubscribe from an SDK event. */
off(event: K, handler: SDKEventHandler): void;
/** Get a snapshot of the current SDK state. */
getState(): SDKState;
/** Programmatically trigger an article access check. */
checkAccess(): Promise;
/** Open the comment panel programmatically. */
openComments(): void;
/** Close the comment panel programmatically. */
closeComments(): void;
/** Check if the user is currently authenticated. */
isLoggedIn(): boolean;
/**
* Return the current access token, or null if not authenticated.
*
* Use this to call your own server-side API routes that need to verify
* the user's identity with the Content Credits API before returning
* protected content — avoids ever sending premium content to the browser
* before access is confirmed.
*/
getToken(): string | null;
/**
* Log the current user out.
*
* Revokes the refresh token on the server (best-effort), clears all local
* auth state, resets SDK state, and emits `auth:logout`.
*/
logout(): Promise;
/** Tear down the SDK — removes all UI, event listeners, and stored state. */
destroy(): void;
/** SDK version string. */
static get version(): string;
}
export { ContentCredits };
export type { Comment, CommentSortBy, SDKConfig, SDKEventHandler, SDKEventName, SDKState, User };