A Step-by-Step Guide to Adding Friendly Captcha in Your Android App
Introduction
CAPTCHAs are widely used to prevent spam and ensure that users interacting with an application are human. Friendly Captcha is an alternative to traditional CAPTCHAs that offers a privacy-friendly, user-friendly, and automated challenge without requiring users to solve puzzles.
This article walks you through the integration of Friendly Captcha in a Jetpack Compose Android app using a ready-to-use composable component.
Step 1: Add Dependencies
Ensure you have WebView support enabled in your project:
In AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
In build.gradle.kts (Module: app)
dependencies {
implementation "androidx.compose.ui:ui:1.5.1"
implementation "androidx.compose.material:material:1.5.1"
implementation "androidx.compose.ui:ui-tooling-preview:1.5.1"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
implementation "androidx.activity:activity-compose:1.7.2"
implementation "androidx.webkit:webkit:1.6.0"
}
Step 2: Add Friendly Captcha API Key
<string name="friendly_captcha_api_key">YOUR_SITE_KEY_HERE</string>
Replace YOUR_SITE_KEY_HERE with the actual API key from your Friendly Captcha account.
Step 3: Implement the CaptchaComponent
This component is responsible for:
- Embedding the CAPTCHA inside a WebView.
- Handling the challenge completion using JavaScript callbacks.
- Enabling the Next button when the challenge is solved.
@Composable
fun CaptchaComponent(
modifier: Modifier = Modifier,
onNextButtonEnabled: (enabled: Boolean) -> Unit,
) {
var captchaResponse by remember { mutableStateOf("") }
val buttonEnabled by remember {
derivedStateOf { captchaResponse.isNotEmpty() }
}
LaunchedEffect(buttonEnabled) {
onNextButtonEnabled(buttonEnabled)
}
FriendlyCaptchaComponent(modifier) { response ->
captchaResponse = response
}
}
What Happens Here?
- We use
remember { mutableStateOf("") }to track the CAPTCHA response. - The button state is updated dynamically with
derivedStateOf { captchaResponse.isNotEmpty() }. - When the CAPTCHA is solved, the Next button becomes enabled.
Step 4: Embed Friendly Captcha in WebView
The WebView loads the CAPTCHA widget using Friendly Captcha’s official JavaScript SDK.
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun FriendlyCaptchaComponent(
modifier: Modifier = Modifier,
onCaptchaSolved: (response: String) -> Unit
) {
val context = LocalContext.current
val captchaHtml = """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<script type="module"
src="https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.13/widget.module.min.js"
async defer></script>
<script nomodule
src="https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.13/widget.min.js"
async defer></script>
<script>
function onReturn(solution) {
Android.onCaptchaSolved(solution);
}
</script>
<style>
.captcha-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 100%;
}
.frc-captcha {
width: 100%;
max-width: 100%;
}
</style>
</head>
<body>
<div class="captcha-container">
<div class="frc-captcha"
data-sitekey="${context.getString(R.string.friendly_captcha_api_key)}"
data-callback="onReturn"></div>
</div>
</body>
</html>
""".trimIndent()
AndroidView(
modifier = modifier.fillMaxWidth(),
factory = { ctx ->
WebView(ctx).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
webChromeClient = WebChromeClient()
webViewClient = WebViewClient()
addJavascriptInterface(object {
@JavascriptInterface
fun onCaptchaSolved(response: String) {
onCaptchaSolved(response)
}
}, "Android")
loadDataWithBaseURL(null, captchaHtml, "text/html", "UTF-8", null)
}
}
)
}
What Happens Here?
- The HTML & JavaScript loads the Friendly Captcha challenge.
- The data-sitekey dynamically fetches the API key from
strings.xml. - The JavaScript interface (
Android.onCaptchaSolved(response)) sends the response back to the app. - Once solved, the Next button is enabled.
Now, place the CaptchaComponent in your UI and connect it to the Next button.
Step 5: Integrate CAPTCHA Component into Your UI
var nextButtonEnabled by remember { mutableStateOf(false) }
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
Text(
text = "Solve the CAPTCHA to continue",
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(bottom = 10.dp)
)
CaptchaComponent(
onNextButtonEnabled = { enabled ->
nextButtonEnabled = enabled
}
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { /* Navigate to the next step */ },
enabled = nextButtonEnabled
) {
Text("Next")
}
}
What Happens Here?
- The Next button is initially disabled.
- Once the CAPTCHA is solved, the
onNextButtonEnabledcallback updatesnextButtonEnabled. - The Next button becomes clickable only after CAPTCHA verification.
Job Offers
Conclusion
You’ve successfully integrated Friendly Captcha into your Jetpack Compose Android app!
This solution is:
- Easy to integrate
- Privacy-friendly (No tracking)
- Works seamlessly with Jetpack Compose
I hope this article has given you a clear and practical way to integrate Friendly Captcha into your Jetpack Compose application. By following these steps, you can enhance your app’s security without compromising user experience.
If you found this guide useful, give it a clap 👏
Happy coding, and may your apps stay bot-free! 😊🎉
This article is previously published on proandroiddev.com.



