Friendly Captcha Examples
Friendly Captcha provides privacy-focused, GDPR-compliant bot protection that automatically solves puzzles without user interaction in most cases. It uses the Friendly Captcha (v2) SDK v0.1.31 and supports three different start modes.
Key Points for Friendly Captcha
Implementation Details
- SDK Version: Uses v0.1.31 from CDN (
https://cdn.jsdelivr.net/npm/@friendlycaptcha/sdk@0.1.31/site.min.js) - Widget Events: Listens for
frc:widget.complete,frc:widget.error, andfrc:widget.expire - Privacy-focused: GDPR compliant, no third-party cookies
- Automatic solving: Puzzles typically solve without user interaction
Start Modes
auto(default): Puzzle starts automatically when widget loadsfocus: Puzzle starts when user focuses on any form input within the same formnone: Puzzle must be started programmatically usingexecute()
Best Practices
- When using start mode
none, you must callexecute()manually to start the puzzle. Forautoandfocusmodes, the puzzle starts automatically andexecute()is not required. - Always handle all three event callbacks (
onToken,onError,onExpired) - Reset widgets after successful form submission to clear stored tokens
- The
reset()method calls the widget’sreset()function and clears token storage
Auto Mode (Default)
Auto mode starts the puzzle automatically when the widget loads, providing the smoothest user experience. However, in cases where a form on the page may not alwayds be submitted by users the focus mode may be more appropriate.
Basic Implementation
import { Captcha } from '@thg-altitude/captcha';
const friendlyCaptcha = new Captcha({
provider: 'friendlyCaptcha',
siteKey: 'your-friendly-captcha-site-key'
});
await friendlyCaptcha.render('captcha-container', 'contact-form', {
startMode: 'auto', // Default - widget starts solving immediately
onToken: (captchaToken) => {
// Called when frc:widget.complete event fires
console.log('Friendly Captcha solved:', captchaToken);
setToken(captchaToken);
},
onError: () => {
// Called when frc:widget.error event fires
showErrorMessage('Security verification failed. Please try again.');
},
onExpired: () => {
// Called when frc:widget.expire event fires
showMessage('Security verification expired. Please complete it again.');
}
});
React Implementation
import { useEffect, useRef, useState } from 'react';
import { Captcha } from '@thg-altitude/captcha';
function ContactForm() {
const captchaRef = useRef<HTMLDivElement>(null);
const captcha = useRef<Captcha>();
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const [error, setError] = useState<string>('');
useEffect(() => {
captcha.current = new Captcha({
provider: 'friendlyCaptcha',
siteKey: 'your-friendly-captcha-site-key'
});
if (captchaRef.current) {
captcha.current.render(captchaRef.current, 'contact-form', {
startMode: 'auto',
onToken: handleCaptchaToken,
onError: handleCaptchaError,
onExpired: handleCaptchaExpired
});
}
return () => {
captcha.current?.reset('contact-form');
};
}, []);
const handleCaptchaToken = (token: string) => {
setCaptchaToken(token);
setError('');
};
const handleCaptchaError = () => {
setCaptchaToken(null);
setError('Security verification failed. Please try again.');
};
const handleCaptchaExpired = () => {
setCaptchaToken(null);
setError('Security verification expired. Please complete it again.');
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!captchaToken) {
setError('Please wait for security verification to complete.');
return;
}
try {
await submitForm(captchaToken);
captcha.current?.reset('contact-form');
setCaptchaToken(null);
} catch (error) {
setError('Form submission failed. Please try again.');
}
};
return (
<form onSubmit={handleSubmit}>
<input type="email" placeholder="Email" required />
<textarea placeholder="Message" required />
{error && <div className="error-message">{error}</div>}
<div ref={captchaRef} id="captcha-container"></div>
<button type="submit" disabled={!captchaToken}>
Submit
</button>
</form>
);
}
Focus Mode
Focus mode starts the puzzle when a user focuses on any input field within the same form as the widget, providing user control over when verification begins.
Basic Implementation
import { Captcha } from '@thg-altitude/captcha';
const friendlyCaptcha = new Captcha({
provider: 'friendlyCaptcha',
siteKey: 'your-friendly-captcha-site-key'
});
await friendlyCaptcha.render('captcha-container', 'login-captcha', {
startMode: 'focus', // Widget starts when user focuses on the form the captcha widget is contained in
onToken: (captchaToken) => {
console.log('Friendly Captcha solved:', captchaToken);
setLoginToken(captchaToken);
},
onError: () => {
showErrorMessage('Security verification failed. Please try again.');
},
onExpired: () => {
showMessage('Security verification expired. Please focus on a form input to restart.');
}
});
React Implementation
import { useEffect, useRef, useState } from 'react';
import { Captcha } from '@thg-altitude/captcha';
function LoginForm() {
const captchaRef = useRef<HTMLDivElement>(null);
const captcha = useRef<Captcha>();
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const [error, setError] = useState<string>('');
useEffect(() => {
captcha.current = new Captcha({
provider: 'friendlyCaptcha',
siteKey: 'your-friendly-captcha-site-key'
});
if (captchaRef.current) {
captcha.current.render(captchaRef.current, 'login-form', {
startMode: 'focus',
onToken: (token) => setCaptchaToken(token),
onError: () => setError('Security verification failed. Please try again.'),
onExpired: () => {
setCaptchaToken(null);
setError('Security verification expired. Please focus on a form input to restart.');
}
});
}
return () => {
captcha.current?.reset('login-form');
};
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!captchaToken) {
setError('Please complete security verification by focusing on a form input.');
return;
}
try {
await submitLoginForm(captchaToken);
captcha.current?.reset('login-form');
setCaptchaToken(null);
} catch (error) {
setError('Login failed. Please try again.');
}
};
return (
<form onSubmit={handleSubmit}>
<input type="email" placeholder="Email" required />
<input type="password" placeholder="Password" required />
{error && <div className="error-message">{error}</div>}
<div ref={captchaRef} id="captcha-container"></div>
<button type="submit" disabled={!captchaToken}>
Login
</button>
</form>
);
}
None Mode (Manual Execution)
None mode requires programmatic execution, similar to invisible CAPTCHA behavior from other providers.
Basic Implementation
import { Captcha } from '@thg-altitude/captcha';
const friendlyCaptcha = new Captcha({
provider: 'friendlyCaptcha',
siteKey: 'your-friendly-captcha-site-key'
});
// Render with manual start mode
await friendlyCaptcha.render('captcha-container', 'checkout-form', {
startMode: 'none', // Widget waits for execute() call
onToken: (token) => {
// Called when frc:widget.complete event fires
// Token received from event.detail.response
submitForm(token);
},
onError: () => {
// Called when frc:widget.error event fires
console.error('Friendly Captcha verification failed');
},
onExpired: () => {
// Called when frc:widget.expire event fires
console.log('Friendly Captcha token expired');
}
});
// Execute on form submission - calls widget.start()
document.getElementById('submit-btn').onclick = async () => {
try {
await friendlyCaptcha.execute('checkout-form');
// Token will be provided via onToken callback when puzzle completes
} catch (error) {
console.error('CAPTCHA verification failed:', error);
}
};
React Implementation
import { useEffect } from 'react';
import { Captcha } from '@thg-altitude/captcha';
function CheckoutForm() {
let friendlyCaptcha;
const captchaId = 'checkout-form';
const containerId = 'friendly-captcha-container';
useEffect(() => {
friendlyCaptcha = new Captcha({
provider: 'friendlyCaptcha',
siteKey: 'your-friendly-captcha-site-key'
});
friendlyCaptcha.render(containerId, captchaId, {
startMode: 'none', // Manual execution - widget won't start until execute() is called
onToken: (captchaToken) => {
// Event handler for frc:widget.complete
submitForm(captchaToken);
},
onError: () => {
// Event handler for frc:widget.error
console.error('Friendly Captcha error:', captchaId);
},
onExpired: () => {
// Event handler for frc:widget.expire
console.log('Friendly Captcha token expired:', captchaId);
}
});
}, []);
const submitForm = async (token) => {
try {
await fetch('/api/checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Captcha-Response': token
},
body: JSON.stringify(formData)
});
} catch (error) {
console.error('Form submission failed:', error);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!friendlyCaptcha) return;
try {
await friendlyCaptcha.execute(captchaId);
// Form submission happens in onToken callback
} catch (error) {
console.error('Friendly Captcha failed:', error);
}
};
return (
<form onSubmit={handleSubmit}>
<input type="text" placeholder="Name" required />
<input type="email" placeholder="Email" required />
<textarea placeholder="Message" required></textarea>
<div id={containerId}></div>
<button type="submit">Submit</button>
</form>
);
}
Vue Implementation
<template>
<form @submit.prevent="handleSubmit">
<input v-model="form.name" type="text" placeholder="Name" required />
<input v-model="form.email" type="email" placeholder="Email" required />
<textarea v-model="form.message" placeholder="Message" required></textarea>
<div id="captchaContainer"></div>
<button type="submit">Submit</button>
</form>
</template>
<script setup>
import { ref, onMounted, onUnmounted, reactive } from 'vue';
import { Captcha } from '@thg-altitude/captcha';
let captcha: Captcha;
const captchaId = 'contact-form';
const containerId = 'captchaContainer';
const form = reactive({
name: '',
email: '',
message: ''
});
onMounted(async () => {
captcha = new Captcha({
provider: 'friendlyCaptcha',
siteKey: 'your-friendly-captcha-site-key'
});
await captcha.render(containerId, captchaId, {
startMode: 'none',
onToken: (captchaToken) => {
submitForm(captchaToken);
},
onError: () => {
console.error('Error rendering captcha:', captchaId);
},
onExpired: () => {
console.log('Captcha expired:', captchaId);
}
});
});
onUnmounted(() => {
captcha?.reset(captchaId);
});
const submitForm = async (token) => {
try {
await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Captcha-Response': token
},
body: JSON.stringify(form)
});
// Reset form
Object.assign(form, { name: '', email: '', message: '' });
captcha.reset(captchaId);
} catch (error) {
console.error('Form submission failed:', error);
}
};
const handleSubmit = async () => {
if (!captcha) return;
try {
await captcha.execute(captchaId);
} catch (error) {
console.error('Friendly Captcha failed:', error);
}
};
</script>