Package Exports
- strainers
- strainers/config
Readme
Strainers
Simple Challenge Page Creator
A powerful React library for creating beautiful, Cloudflare-style challenge pages with hCaptcha, reCAPTCHA, or Turnstile verification. Shows only once per session.
β¨ Features
- π Multiple Captcha Support - hCaptcha, reCAPTCHA, and Cloudflare Turnstile
- π¨ Beautiful UI - Cloudflare-style challenge page design
- π Session Persistence - Shows only once per session (like Cloudflare Challenge Page)
- β Visual Feedback - β on success, β on failure
- π Theme Support - Light, Dark, and Auto themes
- βοΈ React Frameworks - Works with React, Next.js, Remix, Vite, and more
- π¦ TypeScript - Full TypeScript support with types included
- π― Fully Customizable - Custom components, slots, layouts, animations, and styles
π¦ Installation
npm install strainersyarn add strainerspnpm add strainersπ Quick Start
Basic Usage
'use client';
import { ChallengePage } from 'strainers';
export default function Challenge() {
return (
<ChallengePage
config={{
// ===== μμ ― μ ν (ν 1) =====
// β
Cloudflare Turnstile μ¬μ©
widget: 'turnstile',
siteKey: 'YOUR_TURNSTILE_SITE_KEY',
// β
hCaptcha μ¬μ©
// widget: 'hcaptcha',
// siteKey: 'YOUR_HCAPTCHA_SITE_KEY',
// β
Google reCAPTCHA v2 μ¬μ©
// widget: 'recaptcha',
// siteKey: 'YOUR_RECAPTCHA_SITE_KEY',
// ===== 리λλ μ
μ€μ =====
successRedirect: '/dashboard', // μΈμ¦ μ±κ³΅ ν μ΄λν νμ΄μ§
// successRedirect: '/', // κΈ°λ³Έκ°
// ===== μ νμ μ€μ =====
theme: 'auto', // 'light' | 'dark' | 'auto'
title: 'Security Check',
subtitle: 'Please complete the verification to continue.',
}}
callbacks={{
onSuccess: (token) => console.log('β
Verified:', token),
onError: (error) => console.error('β Failed:', error),
}}
/>
);
}π Session Persistence
Strainers automatically saves verification status in sessionStorage. This means:
- β Challenge page shows only once when user first visits
- β Page refresh β Challenge page is skipped
- β Page navigation β Challenge page is skipped
- β Close browser tab β Verification resets (next visit shows challenge)
// To manually clear verification (for testing or logout)
import { clearVerification } from 'strainers';
function LogoutButton() {
const handleLogout = () => {
clearVerification(); // Clear session verification
window.location.href = '/';
};
return <button onClick={handleLogout}>Logout</button>;
}π± Examples
With Next.js App Router
// app/page.tsx - Entry point (Challenge Page)
'use client';
import { ChallengePage } from 'strainers';
export default function Home() {
return (
<ChallengePage
config={{
// Turnstile μ¬μ©
widget: 'turnstile',
siteKey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!,
// hCaptcha μ¬μ©μ:
// widget: 'hcaptcha',
// siteKey: process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!,
// reCAPTCHA μ¬μ©μ:
// widget: 'recaptcha',
// siteKey: process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY!,
theme: 'auto',
successRedirect: '/dashboard',
}}
/>
);
}// app/dashboard/page.tsx - Main page after verification
'use client';
import { clearVerification } from 'strainers';
export default function Dashboard() {
return (
<div>
<h1>π Welcome!</h1>
<p>You have passed the security check.</p>
{/* ν
μ€νΈμ©: μΈμ¦ μ΄κΈ°ν λ²νΌ */}
<button onClick={() => {
clearVerification();
window.location.href = '/';
}}>
π Reset Verification
</button>
</div>
);
}With Remix
// app/routes/_index.tsx
'use client';
import { ChallengePage } from 'strainers';
export default function Index() {
return (
<ChallengePage
config={{
// Turnstile
widget: 'turnstile',
siteKey: 'YOUR_TURNSTILE_KEY',
// hCaptcha
// widget: 'hcaptcha',
// siteKey: 'YOUR_HCAPTCHA_KEY',
// reCAPTCHA
// widget: 'recaptcha',
// siteKey: 'YOUR_RECAPTCHA_KEY',
successRedirect: '/dashboard',
}}
/>
);
}With Vite
// src/pages/Challenge.tsx
import { ChallengePage } from 'strainers';
export function Challenge() {
return (
<ChallengePage
config={{
// Turnstile
widget: 'turnstile',
siteKey: import.meta.env.VITE_TURNSTILE_SITE_KEY,
// hCaptcha
// widget: 'hcaptcha',
// siteKey: import.meta.env.VITE_HCAPTCHA_SITE_KEY,
// reCAPTCHA
// widget: 'recaptcha',
// siteKey: import.meta.env.VITE_RECAPTCHA_SITE_KEY,
theme: 'dark',
successRedirect: '/',
}}
/>
);
}βοΈ Configuration
All Options
<ChallengePage
config={{
// ===== Required =====
widget: 'turnstile', // 'turnstile' | 'hcaptcha' | 'recaptcha'
siteKey: 'YOUR_SITE_KEY',
// ===== Theme & Appearance =====
theme: 'auto', // 'light' | 'dark' | 'auto'
title: 'Security Check', // string | ReactNode | false
subtitle: 'Please verify...', // string | ReactNode | false
logoUrl: '/logo.png', // string | false
widgetSize: 'normal', // 'normal' | 'compact' | 'invisible'
// ===== Redirect =====
successRedirect: '/dashboard', // string | false (to disable)
redirectDelay: 1500, // ms before redirect
// ===== Messages =====
successMessage: 'β
Verified! Redirecting...',
loadingMessage: 'Verifying...',
errorMessages: {
verificationFailed: 'β Verification failed.',
timeout: 'β° Timed out.',
maxAttemptsReached: 'π« Max attempts reached.',
},
// ===== Retry =====
retry: {
enabled: true,
maxAttempts: 3,
delay: 1000,
},
// ===== Branding =====
showBranding: true,
brandingText: 'Protected by Strainers',
}}
/>π¨ Full Customization
Element Customization
<ChallengePage
config={{
widget: 'turnstile', // turnstile, hcaptcha, recaptcha
siteKey: 'KEY',
successRedirect: '/dashboard',
elements: {
container: {
style: { background: 'linear-gradient(135deg, #1a1a2e, #16213e)' },
},
card: {
style: { maxWidth: '500px', borderRadius: '24px' },
},
logo: {
width: 80,
height: 80,
},
title: {
style: { fontSize: '2rem' },
},
subtitle: {
hidden: true, // Hide subtitle
},
retryButton: {
text: 'λ€μ μλ',
},
},
}}
/>Slot Components
<ChallengePage
config={{
widget: 'turnstile', // or 'hcaptcha' or 'recaptcha'
siteKey: 'KEY',
successRedirect: '/dashboard',
slots: {
// Static content
beforeLogo: <div className="badge">Secure</div>,
// Dynamic content
footer: ({ status, attempts }) => (
<p>Status: {status} | Attempts: {attempts}</p>
),
// Custom header (replaces logo, title, subtitle)
header: ({ theme }) => (
<div>
<h1>Custom Header</h1>
</div>
),
},
}}
/>Custom Components
<ChallengePage
config={{
widget: 'hcaptcha', // or 'turnstile' or 'recaptcha'
siteKey: 'KEY',
components: {
Container: ({ children, style }) => (
<main style={style}>{children}</main>
),
Logo: ({ src }) => (
<img src={src} className="custom-logo" />
),
RetryButton: ({ onClick, text }) => (
<button onClick={onClick}>{text}</button>
),
Spinner: () => <div className="custom-loader" />,
},
}}
/>Unstyled Mode
<ChallengePage
config={{
widget: 'recaptcha',
siteKey: 'KEY',
unstyled: true, // Remove ALL default styles
elements: {
container: { className: 'my-custom-container' },
card: { className: 'my-custom-card' },
},
}}
/>πͺ Hooks
useChallenge
import { useChallenge } from 'strainers';
function MyComponent() {
const { state, reset, verify } = useChallenge({
widget: 'turnstile',
siteKey: 'KEY',
});
return (
<div>
<p>Status: {state.status}</p>
<p>Attempts: {state.attempts}</p>
<button onClick={reset}>Reset</button>
</div>
);
}π§ Individual Widget Components
import { CaptchaWidget, TurnstileWidget, HCaptchaWidget, ReCaptchaWidget } from 'strainers';
// Universal widget
<CaptchaWidget
type="turnstile" // or "hcaptcha" or "recaptcha"
siteKey="KEY"
onVerify={(token) => console.log(token)}
/>
// Individual widgets
<TurnstileWidget siteKey="KEY" onVerify={handleVerify} />
<HCaptchaWidget siteKey="KEY" onVerify={handleVerify} />
<ReCaptchaWidget siteKey="KEY" onVerify={handleVerify} />π¦ Publishing to NPM
To publish this package to NPM:
Login to NPM:
npm loginBuild the package:
npm run buildPublish:
npm publish- Note: Ensure verification of the version in
package.jsonbefore publishing. - Use
npm publish --access publicif this is your first time publishing a scoped package or if you want it correctly listed.
- Note: Ensure verification of the version in
π TypeScript
All types are exported:
import type {
StrainersConfig,
CaptchaWidgetType, // 'turnstile' | 'hcaptcha' | 'recaptcha'
ChallengeTheme,
VerificationStatus,
ChallengePageProps,
VerificationCallbacks,
} from 'strainers';π License
Made with β€οΈ by neramc