Skip to main content

React & Next.js

Use the npm package for React and Next.js projects. The SDK is framework-agnostic TypeScript — it works the same in any bundled environment.


Installation

npm install @contentcredits/sdk

React — basic usage

import { useEffect, useRef } from 'react';
import { ContentCredits } from '@contentcredits/sdk';

export function PremiumArticle({ apiKey, children }) {
const ccRef = useRef(null);

useEffect(() => {
// Init after the component has mounted (DOM is ready)
ccRef.current = ContentCredits.init({
apiKey,
contentSelector: '#premium-content',
teaserParagraphs: 2,
enableComments: true,
});

// Cleanup on unmount
return () => {
ccRef.current?.destroy();
ccRef.current = null;
};
}, [apiKey]);

return (
<article>
<p>This paragraph is free to read.</p>
<p>This one too.</p>

<div id="premium-content">
{children}
</div>
</article>
);
}

React Hook

For cleaner component code, wrap the SDK in a custom hook:

// hooks/useContentCredits.ts
import { useEffect, useRef, useCallback } from 'react';
import { ContentCredits } from '@contentcredits/sdk';
import type { SDKConfig, SDKState } from '@contentcredits/sdk';

export function useContentCredits(config: SDKConfig) {
const sdkRef = useRef<ContentCredits | null>(null);

useEffect(() => {
sdkRef.current = ContentCredits.init(config);
return () => {
sdkRef.current?.destroy();
sdkRef.current = null;
};
}, [config.apiKey]);

const on = useCallback(
(event: string, handler: (payload: any) => void) =>
sdkRef.current?.on(event as any, handler),
[]
);

const getState = useCallback(
(): SDKState | null => sdkRef.current?.getState() ?? null,
[]
);

return { on, getState, sdk: sdkRef };
}

Next.js (App Router)

The SDK uses browser APIs (window, document, sessionStorage) so it cannot run during SSR. Use a dynamic import() inside useEffect — this prevents the module from being evaluated by Node.js during prerendering.

The <style> tag below is server-rendered, so content beyond the teaser is hidden at HTML parse time — before any JavaScript runs. This eliminates the flash of full content that would otherwise appear while the SDK bundle loads.

// components/PremiumGate.tsx
'use client';

import { useEffect, useRef } from 'react';

interface PremiumGateProps {
apiKey: string;
children: React.ReactNode;
teaserParagraphs?: number;
}

const GATE_STYLE_ID = 'cc-premium-gate-style';

export function PremiumGate({
apiKey,
children,
teaserParagraphs = 2,
}: PremiumGateProps) {
const ccRef = useRef<{ destroy: () => void } | null>(null);

useEffect(() => {
// Dynamic import prevents SSR from executing the SDK (no `document` in Node)
import('@contentcredits/sdk').then(({ ContentCredits }) => {
ccRef.current = ContentCredits.init({
apiKey,
contentSelector: '#premium-content',
teaserParagraphs,
onAccessGranted: () => {
// Remove the SSR hide-style once access is confirmed
document.getElementById(GATE_STYLE_ID)?.remove();
},
});
});

return () => { ccRef.current?.destroy(); };
}, [apiKey, teaserParagraphs]);

return (
<>
{/* Server-rendered — hides content at HTML parse time before JS runs */}
<style id={GATE_STYLE_ID}>{`
#premium-content > *:nth-child(n+${teaserParagraphs + 1}) { display: none !important; }
`}</style>
{/* --cc-bg controls the gradient fade colour — match your background */}
<div id="premium-content" style={{ '--cc-bg': '#fff' } as React.CSSProperties}>
{children}
</div>
</>
);
}

Then in your page:

// app/articles/[slug]/page.tsx
import { PremiumGate } from '@/components/PremiumGate';

export default async function ArticlePage({ params }) {
const article = await getArticle(params.slug);

return (
<main>
<h1>{article.title}</h1>

{article.isPremium ? (
<PremiumGate apiKey={process.env.NEXT_PUBLIC_CC_API_KEY!}>
<div dangerouslySetInnerHTML={{ __html: article.body }} />
</PremiumGate>
) : (
<div dangerouslySetInnerHTML={{ __html: article.body }} />
)}
</main>
);
}
Why dynamic import?

Top-level import { ContentCredits } from '@contentcredits/sdk' will fail during Next.js prerendering because the SDK immediately accesses document and window when the module is loaded — which don't exist in Node.js. Using import() inside useEffect defers module evaluation to the browser.


Next.js (Pages Router)

// pages/articles/[slug].tsx
import { useEffect, useRef } from 'react';
import type { GetServerSideProps } from 'next';

export default function ArticlePage({ article, apiKey }) {
const ccRef = useRef(null);

useEffect(() => {
if (!article.isPremium) return;

import('@contentcredits/sdk').then(({ ContentCredits }) => {
ccRef.current = ContentCredits.init({
apiKey,
contentSelector: '#premium-content',
});
});

return () => { ccRef.current?.destroy(); };
}, [article.isPremium, apiKey]);

return (
<article>
<h1>{article.title}</h1>
<div
id="premium-content"
dangerouslySetInnerHTML={{ __html: article.body }}
/>
</article>
);
}

Headless mode — fully custom UI

Set headless: true and the SDK becomes a pure logic layer: it never touches the DOM, never injects any UI, and never hides or shows anything. You get callbacks for every paywall state change and action methods to trigger login, purchase, and top-up flows. Your design, your markup, your framework.

Full guide

See the dedicated Headless / Custom UI guide for the complete reference — vanilla JS example, full PremiumGate component, useContentCredits hook, all callbacks, all action methods, and an SDKState field table.

const cc = ContentCredits.init({
apiKey: 'pub_YOUR_KEY',
headless: true,

onLoginRequired() { /* show your login UI */ },
onPurchaseRequired({ requiredCredits }) { /* show unlock UI */ },
onInsufficientCredits({ required, available }) { /* show top-up UI */ },
onAccessGranted() { /* reveal full content */ },
onStateChange(state) { /* drive loading state */ },
});

document.getElementById('btn-login').onclick = () => cc.login();
document.getElementById('btn-purchase').onclick = () => cc.purchase();
document.getElementById('btn-topup').onclick = () => cc.buyMoreCredits();

TypeScript types

The package exports all relevant types:

import type {
SDKConfig,
SDKState,
SDKEventName,
SDKEventHandler,
User,
Comment,
CommentSortBy,
} from '@contentcredits/sdk';

Environment variable for API key

Never hardcode API keys in source files. Use environment variables:

# .env.local
NEXT_PUBLIC_CC_API_KEY=pub_YOUR_API_KEY
ContentCredits.init({
apiKey: process.env.NEXT_PUBLIC_CC_API_KEY!,
...
});

The pub_ prefix API keys are safe for client-side use — they identify your publisher account but don't grant any privileged access. They are similar to Stripe's publishable keys.


SPAs and client-side routing

In single-page apps where the URL changes without a full page reload, call destroy() and reinitialise when navigating to a new article:

let cc: ContentCredits | null = null;

router.on('navigate', (newUrl) => {
cc?.destroy();
cc = ContentCredits.init({
apiKey: 'pub_YOUR_KEY',
articleUrl: newUrl,
contentSelector: '#premium-content',
});
});

The articleUrl option ensures the access check is done for the new article, not the previous one.


Full example

A complete, runnable Next.js 14 blog using this pattern is available in the SDK repository:

examples/nextjs-blog

It includes free and premium articles, the PremiumGate component, and a one-click Vercel deploy button.