androidLogMock;
57 |
58 | @Before
59 | public void init() {
60 | MockitoAnnotations.openMocks(this);
61 | androidLogMock = mockStatic(Log.class);
62 | webView = mock(HCaptchaWebView.class);
63 | webSettings = mock(WebSettings.class);
64 | htmlProvider = mock(IHCaptchaHtmlProvider.class);
65 | when(htmlProvider.getHtml()).thenReturn(MOCK_HTML);
66 | when(webView.getSettings()).thenReturn(webSettings);
67 | when(internalConfig.getHtmlProvider()).thenReturn(htmlProvider);
68 | }
69 |
70 | @After
71 | public void release() {
72 | androidLogMock.close();
73 | }
74 |
75 | @Test
76 | public void test_constructor() {
77 | new HCaptchaWebViewHelper(handler, context, config, internalConfig, captchaVerifier,
78 | webView);
79 | verify(webView).loadDataWithBaseURL(null, MOCK_HTML, "text/html", "UTF-8", null);
80 | verify(webView, times(2)).addJavascriptInterface(any(), anyString());
81 | }
82 |
83 | @Test
84 | public void test_destroy() {
85 | final HCaptchaWebViewHelper webViewHelper = new HCaptchaWebViewHelper(handler, context, config,
86 | internalConfig, captchaVerifier, webView);
87 | final ViewGroup viewParent = mock(ViewGroup.class, withSettings().extraInterfaces(ViewParent.class));
88 | when(webView.getParent()).thenReturn(viewParent);
89 | webViewHelper.destroy();
90 | verify(viewParent).removeView(webView);
91 | verify(webView, times(2)).removeJavascriptInterface(anyString());
92 | }
93 |
94 | @Test
95 | @SuppressWarnings("java:S2699") // expect no exception thrown for public API call
96 | public void test_destroy_webview_parent_null() {
97 | final HCaptchaWebViewHelper webViewHelper = new HCaptchaWebViewHelper(handler, context, config,
98 | internalConfig, captchaVerifier, webView);
99 | webViewHelper.destroy();
100 | }
101 |
102 | @Test
103 | public void test_config_host_used_as_https_base_url() {
104 | final String host = "https://my.awesome.host";
105 | when(config.getBaseUrl()).thenReturn(host);
106 | new HCaptchaWebViewHelper(handler, context, config, internalConfig, captchaVerifier, webView);
107 | verify(webView).loadDataWithBaseURL(host, MOCK_HTML, "text/html", "UTF-8", null);
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/gradle/shared/code-quality.gradle:
--------------------------------------------------------------------------------
1 | checkstyle {
2 | toolVersion = '8.45.1'
3 | }
4 |
5 | tasks.register('checkstyle', Checkstyle) {
6 | description 'Check code standard'
7 | group 'verification'
8 | configFile file("${rootDir}/gradle/config/checkstyle.xml")
9 | source 'src'
10 | include '**/*.java'
11 | exclude '**/gen/**'
12 | classpath = files()
13 | ignoreFailures = false
14 | maxWarnings = 0
15 | }
16 |
17 | pmd {
18 | consoleOutput = true
19 | toolVersion = "6.51.0"
20 | }
21 |
22 | tasks.register('pmd', Pmd) {
23 | ruleSetFiles = files("${project.rootDir}/gradle/config/pmd.xml")
24 | ignoreFailures = false
25 | ruleSets = []
26 | source 'src'
27 | include '**/*.java'
28 | exclude '**/gen/**'
29 | reports {
30 | xml.required = false
31 | xml.outputLocation = file("${project.buildDir}/reports/pmd/pmd.xml")
32 | html.required = true
33 | html.outputLocation = file("$project.buildDir/outputs/pmd/pmd.html")
34 | }
35 | }
36 |
37 | spotbugs {
38 | ignoreFailures = false
39 | showStackTraces = true
40 | showProgress = false
41 | reportLevel = 'high'
42 | excludeFilter = file("${project.rootDir}/gradle/config/findbugs-exclude.xml")
43 | onlyAnalyze = ['com.hcaptcha.sdk.*']
44 | projectName = name
45 | release = version
46 | }
47 |
48 | // enable html report
49 | gradle.taskGraph.beforeTask { task ->
50 | if (task.name.toLowerCase().contains('spotbugs')) {
51 | task.reports {
52 | html.enabled true
53 | xml.enabled true
54 | }
55 | }
56 | }
57 |
58 | // https://www.rallyhealth.com/coding/code-coverage-for-android-testing
59 | tasks.register('jacocoUnitTestReport', JacocoReport) {
60 | dependsOn['testDebugUnitTest']
61 | def coverageSourceDirs = [
62 | "src/main/java"
63 | ]
64 | def javaClasses = fileTree(
65 | dir: "${project.buildDir}/intermediates/javac/debug/classes",
66 | excludes: [
67 | '**/R.class',
68 | '**/R$*.class',
69 | '**/BuildConfig.*',
70 | '**/Manifest*.*'
71 | ]
72 | )
73 |
74 | classDirectories.from files([javaClasses])
75 | additionalSourceDirs.from files(coverageSourceDirs)
76 | sourceDirectories.from files(coverageSourceDirs)
77 | executionData.from = "${project.buildDir}/jacoco/testDebugUnitTest.exec"
78 |
79 | reports {
80 | xml.required = true
81 | html.required = true
82 | }
83 |
84 | inputs.files(tasks.named("testDebugUnitTest").get().outputs)
85 | }
86 |
87 | check.dependsOn('checkstyle', 'pmd', 'jacocoUnitTestReport')
88 |
89 | sonarqube {
90 | properties {
91 | property "sonar.projectKey", "hCaptcha_hcaptcha-android-sdk"
92 | property "sonar.organization", "hcaptcha"
93 | property "sonar.host.url", "https://sonarcloud.io"
94 |
95 | property "sonar.language", "java"
96 | property "sonar.sourceEncoding", "utf-8"
97 |
98 | property "sonar.sources", "src/main"
99 | property "sonar.java.binaries", layout.buildDirectory.dir("intermediates/javac/debug/compileDebugJavaWithJavac/classes").get().asFile.absolutePath
100 | property "sonar.tests", ["src/test/", "../test/src/androidTest/"]
101 |
102 | property "sonar.android.lint.report", layout.buildDirectory.dir("outputs/lint-results.xml").get().asFile.absolutePath
103 | property "sonar.java.spotbugs.reportPaths", ["debug", "release"].collect { layout.buildDirectory.dir("reports/spotbugs/${it}.xml").get().asFile.absolutePath }
104 | property "sonar.java.pmd.reportPaths", layout.buildDirectory.dir("reports/pmd/pmd.xml").get().asFile.absolutePath
105 | property "sonar.java.checkstyle.reportPaths", layout.buildDirectory.dir("reports/checkstyle/checkstyle.xml").get().asFile.absolutePath
106 | property "sonar.coverage.jacoco.xmlReportPaths", layout.buildDirectory.dir("reports/jacoco/jacocoUnitTestReport.xml").get().asFile.absolutePath
107 | }
108 | }
109 |
110 | project.tasks.named("sonarqube").configure { dependsOn "check" }
111 |
112 |
--------------------------------------------------------------------------------
/sdk/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id "com.android.library"
3 | id "maven-publish"
4 | id "pmd"
5 | id "jacoco"
6 | id "checkstyle"
7 | id "com.github.spotbugs" version "5.2.3"
8 | id "org.owasp.dependencycheck" version "7.1.1"
9 | id "org.sonarqube" version "3.4.0.2513"
10 | }
11 |
12 | ext {
13 | maxAarSizeKb = 200
14 | }
15 |
16 | android {
17 | compileSdk 35
18 | namespace 'com.hcaptcha.sdk'
19 |
20 | buildFeatures {
21 | buildConfig true
22 | }
23 |
24 | defaultConfig {
25 | minSdkVersion 16
26 | targetSdkVersion 35
27 |
28 | // See https://developer.android.com/studio/publish/versioning
29 | // versionCode must be integer and be incremented by one for every new update
30 | // android system uses this to prevent downgrades
31 | versionCode 56
32 |
33 | // version number visible to the user
34 | // should follow semantic versioning (See https://semver.org)
35 | versionName "4.4.0"
36 |
37 | buildConfigField 'String', 'VERSION_NAME', "\"${defaultConfig.versionName}_${defaultConfig.versionCode}\""
38 |
39 | consumerProguardFiles "consumer-rules.pro"
40 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
41 | }
42 |
43 | compileOptions {
44 | // Sets Java compatibility to Java 8
45 | sourceCompatibility JavaVersion.VERSION_1_8
46 | targetCompatibility JavaVersion.VERSION_1_8
47 | }
48 |
49 | buildTypes {
50 | release {
51 | minifyEnabled true
52 | }
53 | }
54 |
55 | testOptions {
56 | unitTests {
57 | returnDefaultValues = true
58 | includeAndroidResources = true
59 | }
60 | }
61 |
62 | publishing {
63 | singleVariant('release') {
64 | withSourcesJar()
65 | withJavadocJar()
66 | }
67 | }
68 | }
69 |
70 | dependencies {
71 | //noinspection GradleDependency
72 | implementation 'androidx.appcompat:appcompat:1.3.1'
73 | //noinspection GradleDependency
74 | implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.5' // max version https://github.com/FasterXML/jackson-databind/issues/3657
75 | compileOnly 'org.projectlombok:lombok:1.18.32'
76 | annotationProcessor 'org.projectlombok:lombok:1.18.32'
77 |
78 | testImplementation 'junit:junit:4.13.2'
79 | testImplementation 'org.mockito:mockito-inline:4.8.1'
80 | testImplementation 'org.skyscreamer:jsonassert:1.5.1'
81 |
82 | compileOnly 'com.google.code.findbugs:annotations:3.0.1'
83 | }
84 |
85 | project.afterEvaluate {
86 | publishing {
87 | repositories {
88 | }
89 |
90 | publications {
91 | release(MavenPublication) {
92 | from components.release
93 |
94 | groupId = 'com.hcaptcha'
95 | artifactId = 'sdk'
96 | version = android.defaultConfig.versionName
97 |
98 | pom {
99 | name = 'Android SDK hCaptcha'
100 | description = 'This SDK provides a wrapper for hCaptcha and is a drop-in replacement for the SafetyNet reCAPTCHA API.'
101 | url = 'https://github.com/hCaptcha/hcaptcha-android-sdk'
102 | licenses {
103 | license {
104 | name = 'MIT License'
105 | url = 'https://github.com/hCaptcha/hcaptcha-android-sdk/blob/main/LICENSE'
106 | }
107 | }
108 | scm {
109 | connection = 'scm:git:git://github.com/hCaptcha/hcaptcha-android-sdk.git'
110 | developerConnection = 'scm:git:ssh://github.com:hCaptcha/hcaptcha-android-sdk.git'
111 | url = 'https://github.com/hCaptcha/hcaptcha-android-sdk'
112 | }
113 | }
114 | }
115 | }
116 | }
117 | }
118 |
119 | apply from: "$rootProject.projectDir/gradle/shared/code-quality.gradle"
120 | apply from: "$rootProject.projectDir/gradle/shared/size-check.gradle"
121 | apply from: "$rootProject.projectDir/gradle/shared/html-java-gen.gradle"
122 |
--------------------------------------------------------------------------------
/sdk/src/main/java/com/hcaptcha/sdk/IHCaptcha.java:
--------------------------------------------------------------------------------
1 | package com.hcaptcha.sdk;
2 |
3 | import lombok.NonNull;
4 |
5 | /**
6 | * hCaptcha client which allows invoking of challenge completion and listening for the result.
7 | *
8 | *
9 | * Usage example:
10 | * 1. Get a client either using the site key or customize by passing a config:
11 | * client = HCaptcha.getClient(this).verifyWithHCaptcha(YOUR_API_SITE_KEY)
12 | * client = HCaptcha.getClient(this).verifyWithHCaptcha({@link com.hcaptcha.sdk.HCaptchaConfig})
13 | *
14 | * // Improve cold start by setting up the hCaptcha client
15 | * client = HCaptcha.getClient(this).setup({@link com.hcaptcha.sdk.HCaptchaConfig})
16 | * // ui rendering...
17 | * // user form fill up...
18 | * // ready for human verification
19 | * client.verifyWithHCaptcha();
20 | * 2. Listen for the result and error events:
21 | *
22 | * client.addOnSuccessListener(new OnSuccessListener{@literal <}HCaptchaTokenResponse{@literal >}() {
23 | * {@literal @}Override
24 | * public void onSuccess(HCaptchaTokenResponse response) {
25 | * String userResponseToken = response.getTokenResult();
26 | * Log.d(TAG, "hCaptcha token: " + userResponseToken);
27 | * // Validate the user response token using the hCAPTCHA siteverify API
28 | * }
29 | * })
30 | * .addOnFailureListener(new OnFailureListener() {
31 | * {@literal @}Override
32 | * public void onFailure(HCaptchaException e) {
33 | * Log.d(TAG, "hCaptcha failed: " + e.getMessage() + "(" + e.getStatusCode() + ")");
34 | * }
35 | * });
36 | *
37 | */
38 | public interface IHCaptcha {
39 |
40 | /**
41 | * Prepare the client which allows to display a challenge dialog
42 | *
43 | * @return new {@link IHCaptcha} object
44 | */
45 | IHCaptcha setup();
46 |
47 | /**
48 | * Constructs a new client which allows to display a challenge dialog
49 | *
50 | * @param siteKey The hCaptcha site-key. Get one here hcaptcha.com
51 | * @return new {@link HCaptcha} object
52 | */
53 | IHCaptcha setup(@NonNull String siteKey);
54 |
55 | /**
56 | * Constructs a new client which allows to display a challenge dialog
57 | *
58 | * @param config Config to customize: size, theme, locale, endpoint, rqdata, etc.
59 | * @return new {@link HCaptcha} object
60 | */
61 | IHCaptcha setup(@NonNull HCaptchaConfig config);
62 |
63 | /**
64 | * Shows a captcha challenge dialog to be completed by the user
65 | *
66 | * @return {@link HCaptcha}
67 | */
68 | IHCaptcha verifyWithHCaptcha();
69 |
70 | /**
71 | * Shows a captcha challenge dialog to be completed by the user
72 | *
73 | * @param siteKey The hCaptcha site-key. Get one here hcaptcha.com
74 | * @return {@link IHCaptcha}
75 | */
76 | IHCaptcha verifyWithHCaptcha(@NonNull String siteKey);
77 |
78 | /**
79 | * Shows a captcha challenge dialog to be completed by the user
80 | *
81 | * @param config Config to customize: size, theme, locale, endpoint, rqdata, etc.
82 | * @return {@link HCaptcha}
83 | */
84 | IHCaptcha verifyWithHCaptcha(@NonNull HCaptchaConfig config);
85 |
86 | /**
87 | * Shows a captcha challenge dialog to be completed by the user
88 | *
89 | * @param verifyParams Parameters for verification including phone prefix and phone number
90 | * @return {@link IHCaptcha}
91 | */
92 | IHCaptcha verifyWithHCaptcha(@NonNull HCaptchaVerifyParams verifyParams);
93 |
94 | /**
95 | * Shows a captcha challenge dialog to be completed by the user
96 | *
97 | * @param config Config to customize: size, theme, locale, endpoint, rqdata, etc.
98 | * @param verifyParams Parameters for verification including phone prefix and phone number
99 | * @return {@link IHCaptcha}
100 | */
101 | IHCaptcha verifyWithHCaptcha(@NonNull HCaptchaConfig config, @NonNull HCaptchaVerifyParams verifyParams);
102 |
103 | /**
104 | * Force stop verification and clear hCaptcha state.
105 | */
106 | void reset();
107 |
108 | /**
109 | * Fully destroy resources, including the underlying WebView and any preloaded state.
110 | * Use this in Activity/Fragment teardown to prevent retaining the host context.
111 | */
112 | void destroy();
113 | }
114 |
--------------------------------------------------------------------------------
/test/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 |
4 | if (project.hasProperty("testingMinimizedBuild")) {
5 | apply plugin: 'com.slack.keeper'
6 | }
7 |
8 | android {
9 | compileSdk 34
10 | namespace 'com.hcaptcha.sdk.test'
11 |
12 | defaultConfig {
13 | applicationId "com.hcaptcha.sdk.test"
14 | minSdkVersion 23
15 | targetSdkVersion 34
16 | versionCode 1
17 | versionName "1.0"
18 |
19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
20 | }
21 |
22 | buildTypes {
23 | release {
24 | signingConfig signingConfigs.debug
25 | minifyEnabled true
26 | shrinkResources true
27 | proguardFiles getDefaultProguardFile('proguard-android.txt'), '../sdk/consumer-rules.pro'
28 | }
29 | }
30 |
31 | compileOptions {
32 | sourceCompatibility JavaVersion.VERSION_1_8
33 | targetCompatibility JavaVersion.VERSION_1_8
34 | }
35 |
36 | testBuildType project.hasProperty("testingMinimizedBuild") ? "release" : "debug"
37 | testOptions {
38 | animationsDisabled = true
39 | }
40 |
41 | buildFeatures {
42 | compose = true
43 | }
44 |
45 | kotlinOptions {
46 | jvmTarget = JavaVersion.VERSION_1_8
47 | }
48 |
49 | composeOptions {
50 | kotlinCompilerExtensionVersion = "$compose_version"
51 | }
52 | }
53 |
54 | if (project.hasProperty("testingMinimizedBuild")) {
55 | project.afterEvaluate {
56 | tasks.register("postInferReleaseAndroidTestKeepRulesForKeeper") {
57 | doLast {
58 | def sourceFile = file("${projectDir}/test-proguard-rules.pro")
59 | def destinationFile = fileTree(dir: "${project.buildDir}/intermediates/keeper", include: '**/inferredKeepRules.pro').find { true }
60 |
61 | if (sourceFile.exists() && destinationFile.exists()) {
62 | def sourceText = sourceFile.text
63 | destinationFile << sourceText
64 | println("Rules from of ${sourceFile} appended too keeper")
65 | } else {
66 | if (!sourceFile.exists()) {
67 | throw new GradleException("Proguard file does not exist: ${sourceFile}")
68 | }
69 | if (!destinationFile.exists()) {
70 | throw new GradleException("Keeper's proguard file does not exist: ${destinationFile}")
71 | }
72 | }
73 | }
74 | }
75 |
76 | tasks.named("inferReleaseAndroidTestKeepRulesForKeeper").configure {
77 | finalizedBy(tasks.named("postInferReleaseAndroidTestKeepRulesForKeeper"))
78 | }
79 | }
80 | }
81 |
82 | androidComponents {
83 | beforeVariants(selector().all()) { variantBuilder ->
84 | if (variantBuilder.name == "release") {
85 | variantBuilder.registerExtension(
86 | com.slack.keeper.KeeperVariantMarker.class,
87 | com.slack.keeper.KeeperVariantMarker.INSTANCE)
88 | }
89 | }
90 | }
91 |
92 | dependencies {
93 | testImplementation 'junit:junit:4.13.2'
94 |
95 | implementation project(path: ':sdk')
96 | implementation 'androidx.appcompat:appcompat:1.6.1'
97 | androidTestImplementation 'androidx.fragment:fragment-testing:1.6.2'
98 | androidTestImplementation 'androidx.test:core:1.5.0'
99 | androidTestImplementation 'androidx.test:rules:1.5.0'
100 | androidTestImplementation 'androidx.test:runner:1.5.2'
101 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
102 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
103 | androidTestImplementation 'androidx.test.espresso:espresso-web:3.5.1'
104 | androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
105 | androidTestImplementation 'org.mockito:mockito-android:5.3.1'
106 | androidTestImplementation 'com.fasterxml.jackson.core:jackson-databind:2.13.5' // max version https://github.com/FasterXML/jackson-databind/issues/3657
107 |
108 | implementation project(path: ':compose-sdk')
109 | implementation 'androidx.compose.material3:material3:1.2.1'
110 | implementation "androidx.compose.ui:ui:$compose_version"
111 | implementation "androidx.compose.foundation:foundation-layout-android:$compose_version"
112 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4-android:1.6.8'
113 | }
114 |
--------------------------------------------------------------------------------
/gradle/config/cve.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 | ^pkg:generic/openssl@.*$
11 | CVE-1999-0428
12 |
13 |
14 |
19 | ^pkg:generic/openssl@.*$
20 | CVE-2009-0590
21 |
22 |
23 |
27 | ^pkg:generic/openssl@.*$
28 | CVE-2019-0190
29 |
30 |
31 |
36 | ^pkg:generic/openssl@.*$
37 | CVE-2019-1551
38 |
39 |
40 |
45 | 636cf935a0fd1451657a4112974b3500cce3ab84
46 | CVE-2019-11065
47 |
48 |
49 |
54 | 636cf935a0fd1451657a4112974b3500cce3ab84
55 | CVE-2019-15052
56 |
57 |
58 |
62 | ^pkg:maven/org\.bouncycastle/bcprov\-jdk15on@.*$
63 | CVE-2017-13098
64 |
65 |
66 |
70 | ^pkg:maven/org\.bouncycastle/bcprov\-jdk15on@.*$
71 | CVE-2018-1000180
72 |
73 |
74 |
78 | ^pkg:maven/org\.bouncycastle/bcprov\-jdk15on@.*$
79 | CVE-2018-1000613
80 |
81 |
82 |
86 | ^pkg:maven/org\.apache\.commons/commons\-compress@.*$
87 | CVE-2018-11771
88 |
89 |
90 |
94 | ^pkg:maven/org\.apache\.commons/commons\-compress@.*$
95 | CVE-2018-1324
96 |
97 |
98 |
102 | ^pkg:maven/com\.google\.protobuf/protobuf\-java@.*$
103 | CVE-2015-5237
104 |
105 |
106 |
110 | ^pkg:maven/com\.google\.guava/guava@.*$
111 | CVE-2018-10237
112 |
113 |
114 |
--------------------------------------------------------------------------------
/example-app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
7 |
9 |
11 |
13 |
15 |
17 |
19 |
21 |
23 |
25 |
27 |
29 |
31 |
33 |
35 |
37 |
39 |
41 |
43 |
45 |
47 |
49 |
51 |
53 |
55 |
57 |
59 |
61 |
63 |
65 |
67 |
69 |
70 |
--------------------------------------------------------------------------------
/example-compose-app/src/main/java/com/hcaptcha/example/compose/ComposeActivity.kt:
--------------------------------------------------------------------------------
1 | package com.hcaptcha.example.compose
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.layout.*
8 | import androidx.compose.foundation.layout.Arrangement
9 | import androidx.compose.runtime.*
10 | import androidx.compose.material3.Button
11 | import androidx.compose.material3.Checkbox
12 | import androidx.compose.material3.CircularProgressIndicator
13 | import androidx.compose.material3.Text
14 | import androidx.compose.material3.TextField
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.graphics.Color
18 | import androidx.compose.ui.unit.dp
19 | import com.hcaptcha.sdk.HCaptchaCompose
20 | import com.hcaptcha.sdk.HCaptchaConfig
21 | import com.hcaptcha.sdk.HCaptchaEvent
22 | import com.hcaptcha.sdk.HCaptchaResponse
23 | import com.hcaptcha.sdk.HCaptchaSize
24 |
25 | class ComposeActivity : ComponentActivity() {
26 |
27 | private enum class CaptchaState { Idle, Started, Loaded }
28 |
29 | override fun onCreate(savedInstanceState: Bundle?) {
30 | super.onCreate(savedInstanceState)
31 | setContent {
32 | var hideDialog by remember { mutableStateOf(false) }
33 | var captchaState by remember { mutableStateOf(CaptchaState.Idle) }
34 | var text by remember { mutableStateOf("") }
35 |
36 | val hCaptchaConfig = remember(hideDialog) {
37 | HCaptchaConfig.builder()
38 | .siteKey("10000000-ffff-ffff-ffff-000000000001")
39 | .size(if (hideDialog) HCaptchaSize.INVISIBLE else HCaptchaSize.NORMAL)
40 | .hideDialog(hideDialog)
41 | .diagnosticLog(true)
42 | .build()
43 | }
44 |
45 | if (captchaState != CaptchaState.Idle) {
46 | HCaptchaCompose(hCaptchaConfig) { result ->
47 | val message = when (result) {
48 | is HCaptchaResponse.Success -> {
49 | captchaState = CaptchaState.Idle
50 | "Success: ${result.token}"
51 | }
52 | is HCaptchaResponse.Failure -> {
53 | captchaState = CaptchaState.Idle
54 | "Failure: ${result.error.message}"
55 | }
56 | is HCaptchaResponse.Event -> {
57 | if (result.event == HCaptchaEvent.Opened) {
58 | captchaState = CaptchaState.Loaded
59 | }
60 | "Event: ${result.event}"
61 | }
62 | }
63 | text += "\n${message}"
64 | println(message)
65 | }
66 | }
67 |
68 | CaptchaControlUI(
69 | hideDialog = hideDialog,
70 | onHideDialogChanged = { hideDialog = it },
71 | text = text,
72 | onVerifyClick = {
73 | captchaState = CaptchaState.Started
74 | text = ""
75 | },
76 | showProgress = captchaState == CaptchaState.Started
77 | )
78 | }
79 | }
80 |
81 | @Composable
82 | private fun CaptchaControlUI(
83 | hideDialog: Boolean,
84 | onHideDialogChanged: (Boolean) -> Unit,
85 | text: String,
86 | onVerifyClick: () -> Unit,
87 | showProgress: Boolean
88 | ) {
89 | Column(
90 | modifier = Modifier
91 | .fillMaxSize()
92 | .padding(WindowInsets.systemBars.asPaddingValues())
93 | .padding(16.dp),
94 | verticalArrangement = Arrangement.Bottom
95 | ) {
96 | TextField(
97 | value = text,
98 | onValueChange = {},
99 | placeholder = { Text("Verification result will be here...") },
100 | readOnly = true,
101 | modifier = Modifier
102 | .fillMaxWidth()
103 | .height(200.dp)
104 | .background(Color.Gray)
105 | )
106 |
107 | Spacer(modifier = Modifier.weight(1f))
108 |
109 | Row(verticalAlignment = Alignment.CenterVertically) {
110 | Checkbox(
111 | checked = hideDialog,
112 | onCheckedChange = onHideDialogChanged
113 | )
114 | Text(text = "Hide Dialog (Passive Site Key)")
115 | }
116 |
117 | Button(
118 | onClick = onVerifyClick,
119 | modifier = Modifier
120 | .fillMaxWidth()
121 | .padding(vertical = 16.dp)
122 | ) {
123 | Text(text = "Verify with HCaptcha")
124 | }
125 |
126 | if (showProgress) {
127 | Box(
128 | contentAlignment = Alignment.Center,
129 | modifier = Modifier.fillMaxSize()
130 | ) {
131 | CircularProgressIndicator(
132 | modifier = Modifier.width(64.dp),
133 | )
134 | }
135 | }
136 | }
137 | }
138 | }
139 |
140 |
--------------------------------------------------------------------------------
/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaTest.java:
--------------------------------------------------------------------------------
1 | package com.hcaptcha.sdk;
2 |
3 | import static com.hcaptcha.sdk.AssertUtil.waitHCaptchaWebViewToken;
4 | import static org.junit.Assert.assertEquals;
5 | import static org.junit.Assert.assertTrue;
6 | import static org.junit.Assert.fail;
7 |
8 | import android.app.Activity;
9 | import android.os.Looper;
10 |
11 | import androidx.test.core.app.ActivityScenario;
12 | import androidx.test.ext.junit.rules.ActivityScenarioRule;
13 |
14 | import com.hcaptcha.sdk.tasks.OnSuccessListener;
15 | import com.hcaptcha.sdk.test.TestActivity;
16 | import com.hcaptcha.sdk.test.TestNonFragmentActivity;
17 |
18 | import org.junit.Rule;
19 | import org.junit.Test;
20 |
21 | import java.util.concurrent.CountDownLatch;
22 | import java.util.concurrent.TimeUnit;
23 |
24 | public class HCaptchaTest {
25 | private static final long AWAIT_CALLBACK_MS = 5000;
26 | private static final long E2E_AWAIT_CALLBACK_MS = AWAIT_CALLBACK_MS * 5;
27 |
28 | @Rule
29 | public ActivityScenarioRule rule = new ActivityScenarioRule<>(TestActivity.class);
30 |
31 | final HCaptchaConfig config = HCaptchaConfig.builder()
32 | .siteKey("10000000-ffff-ffff-ffff-000000000001")
33 | .hideDialog(true)
34 | .tokenExpiration(1)
35 | .build();
36 |
37 | final HCaptchaInternalConfig internalConfig = HCaptchaInternalConfig.builder()
38 | .htmlProvider(new HCaptchaTestHtml())
39 | .build();
40 |
41 | @Test
42 | public void testExpiredAfterSuccess() throws Exception {
43 | final CountDownLatch latch = new CountDownLatch(2);
44 |
45 | final ActivityScenario scenario = rule.getScenario();
46 | scenario.onActivity(activity -> HCaptcha.getClient(activity, internalConfig)
47 | .verifyWithHCaptcha(config)
48 | .addOnSuccessListener(response -> latch.countDown())
49 | .addOnFailureListener(exception -> {
50 | assertEquals(HCaptchaError.TOKEN_TIMEOUT, exception.getHCaptchaError());
51 | latch.countDown();
52 | }));
53 |
54 | waitHCaptchaWebViewToken(latch, AWAIT_CALLBACK_MS);
55 | }
56 |
57 | @Test
58 | public void webViewSessionTimeoutSuppressed() throws Exception {
59 | final CountDownLatch latch = new CountDownLatch(1);
60 |
61 | final ActivityScenario scenario = rule.getScenario();
62 | scenario.onActivity(activity -> HCaptcha.getClient(activity, internalConfig)
63 | .verifyWithHCaptcha(config)
64 | .addOnSuccessListener(response -> {
65 | response.markUsed();
66 | latch.countDown();
67 | })
68 | .addOnFailureListener(exception -> fail("Session timeout should not be happened")));
69 |
70 | waitHCaptchaWebViewToken(latch, AWAIT_CALLBACK_MS);
71 | }
72 |
73 | @Test
74 | public void removedListenerShouldNotBeCalled() throws Exception {
75 | final CountDownLatch latch = new CountDownLatch(1);
76 |
77 | final OnSuccessListener listener1 = response -> {
78 | fail("Listener1 should never be called");
79 | };
80 |
81 | final OnSuccessListener listener2 = response -> {
82 | response.markUsed();
83 | latch.countDown();
84 | };
85 |
86 | final ActivityScenario scenario = rule.getScenario();
87 | scenario.onActivity(activity -> HCaptcha.getClient(activity, internalConfig)
88 | .verifyWithHCaptcha(config)
89 | .addOnSuccessListener(listener1)
90 | .addOnFailureListener(exception -> fail("Session timeout should not be happened"))
91 | .removeOnSuccessListener(listener1)
92 | .addOnSuccessListener(listener2));
93 |
94 | waitHCaptchaWebViewToken(latch, AWAIT_CALLBACK_MS);
95 | }
96 |
97 | @Test
98 | public void e2eWithDebugTokenFragmentDialog() throws Exception {
99 | final CountDownLatch latch = new CountDownLatch(1);
100 |
101 | final ActivityScenario scenario = rule.getScenario();
102 | scenario.onActivity(activity -> HCaptcha.getClient(activity)
103 | .verifyWithHCaptcha(config.toBuilder().hideDialog(false).build())
104 | .addOnSuccessListener(response -> {
105 | response.markUsed();
106 | latch.countDown();
107 | })
108 | .addOnFailureListener(exception -> fail("No errors expected but received: " + exception.getHCaptchaError())));
109 |
110 | assertTrue(latch.await(E2E_AWAIT_CALLBACK_MS, TimeUnit.MILLISECONDS));
111 | }
112 |
113 | @Test
114 | public void e2eWithDebugTokenHeadlessWebView() throws Exception {
115 | final CountDownLatch latch = new CountDownLatch(1);
116 |
117 | final ActivityScenario scenario = rule.getScenario();
118 | scenario.onActivity(activity -> HCaptcha.getClient(activity)
119 | .verifyWithHCaptcha(config.toBuilder().hideDialog(true).build())
120 | .addOnSuccessListener(response -> {
121 | response.markUsed();
122 | latch.countDown();
123 | })
124 | .addOnFailureListener(exception -> fail("No errors expected")));
125 |
126 | assertTrue(latch.await(E2E_AWAIT_CALLBACK_MS, TimeUnit.MILLISECONDS));
127 | }
128 |
129 | @Test(expected = IllegalStateException.class)
130 | public void badActivity() {
131 | Looper.prepare();
132 | final Activity activity = new TestNonFragmentActivity();
133 |
134 | HCaptcha.getClient(activity)
135 | .verifyWithHCaptcha(config.toBuilder().hideDialog(false).diagnosticLog(true).build())
136 | .addOnSuccessListener(response -> fail("No token expected"))
137 | .addOnFailureListener(e -> fail("Wrong failure reason: " + e.getHCaptchaError()));
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/test/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | # 4.4.0
4 |
5 | - Feat: add explicit `HCaptcha.destroy()` API to fully tear down WebView and related resources.
6 | - Fix: handle target="_blank" links by opening in browser app
7 |
8 | # 4.3.2
9 |
10 | - Fix: backward compatibility with HCaptchaConfig.rqdata
11 |
12 | # 4.3.1
13 |
14 | - Fix: enable incremental builds for HTML-to-Java generation tasks
15 | - Fix: correct nullability annotation to prevent `NullPointerException`
16 | - Docs: add note about `reset` call for updated HCaptchaVerifyParams on next verify call
17 |
18 |
19 |
20 | # 4.3.0
21 |
22 | - Feature: implement HCaptchaVerifyParams with phone prefix/number support
23 |
24 | # 4.2.4
25 |
26 | - Fix: loading dialog background depends on `HCaptchaConfig.theme` (#201)
27 |
28 | # 4.2.3
29 |
30 | - Fix: java.util.ConcurrentModificationException on multiple hCaptcha verify calls (#198)
31 | - Fix: accept hostname instead of url in host config (#196)
32 | - Fix: Activity memory leak (#195)
33 |
34 | # 4.2.2
35 |
36 | - Chore: Restructured example-compose-app (#182)
37 | - Feature: handle `sms:` schemas in WebView (#192)
38 |
39 | # 4.2.1
40 |
41 | - Fix: java.lang.NullPointerException: during WebView preparation (#189)
42 |
43 | # 4.2.0
44 |
45 | - Fix: java.lang.IllegalStateException: The specified child already has a parent for reloadedWebView in compose-sdk
46 |
47 | # 4.1.2
48 |
49 | - Fix: double call with CHALLENGE_CLOSED error
50 | - Fix: broken retryPredicate config
51 |
52 | # 4.1.1
53 |
54 | - Fix: back button should cancel hCaptcha in compose-sdk
55 |
56 | # 4.1.0
57 |
58 | - Feat: preload WebView on `setup` call
59 |
60 | # 4.0.5
61 |
62 | - compose-sdk: set minSdk to 21
63 |
64 | # 4.0.4
65 |
66 | - Downgrade: jackson-databind to 2.13.* (#170)
67 |
68 | # 4.0.3
69 |
70 | - Upgrade: third-party dependencies (lombok, jackson-databind) (#167)
71 |
72 | # 4.0.2
73 |
74 | - Fix: passive site keys (hideDialog=true) broken for `compose-sdk`
75 |
76 | # 4.0.1
77 |
78 | - Feat: release of `compose-sdk`
79 |
80 | # 4.0.0
81 |
82 | - Feat (breaking change): accept `HCaptcha.getClient(Activity)` for passive sitekeys. (#112)
83 |
84 | # 3.11.0
85 |
86 | - Fix: handle null `internalConfig` in args for HCaptchaDialogFragment (#140)
87 | - Feature: drop diagnostic logs from production code (#139)
88 | - Fix: wrong language used in `values-be/strings.xml` (#138)
89 | - Fix: misleading exception on missing `siteKey` (#137)
90 | - Fix: calling `webView.loadUrl` on destroyed `WebView` (#136)
91 |
92 | # 3.10.0
93 |
94 | - Fix: crash on insecure HTTP request handling
95 | - Feat: new error code `INSECURE_HTTP_REQUEST_ERROR`
96 |
97 | # 3.9.1
98 |
99 | - Fix: add missing ProGuard rules for enums
100 |
101 | # 3.9.0
102 |
103 | - Feature: add config to control WebView hardware acceleration `HCaptchaConfig.disableHardwareAcceleration`
104 | - Fix: removed unsafe cast with improved public api
105 |
106 | # 3.8.2
107 |
108 | - Bugfix: handle BadParcelableException when hCaptcha fragment needs to be recreated due to app resume
109 |
110 | # 3.8.1
111 |
112 | - Bugfix: report error when missing WebView provider
113 |
114 | # 3.8.0
115 |
116 | - Feat: new `HCaptcha.reset` to force stop verification and release all resources.
117 |
118 | # 3.7.0
119 |
120 | - Feat: new `HCaptchaConfig.orientation` to set either `portrait` or `landscape` challenge orientation.
121 |
122 | # 3.6.0
123 |
124 | - Feat: new `HCaptcha.removeAllListener` and `HCaptcha.removeOn[Success|Failure|Open]Listener(listener)` to remove all or specific listener.
125 |
126 | # 3.5.2
127 |
128 | - Bugfix: java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState, on `verifyWithHCaptcha`
129 |
130 | # 3.5.1
131 |
132 | - Bugfix: Parcelable encountered IOException writing serializable object (name = com.hcaptcha.sdk.HCaptchaConfig) ([#94](https://github.com/hCaptcha/hcaptcha-android-sdk/issues/94))
133 |
134 | # 3.5.0
135 |
136 | - Deprecated: `HCaptchaConfig.apiEndpoint` replaced with `HCaptchaConfig.jsSrc` option
137 |
138 | # 3.4.0
139 |
140 | - Feat: new `HCaptchaConfig.retryPredicate` which allows conditional automatic retry
141 | - Deprecated: `HCaptchaConfig.resetOnTimeout` replaced by more generic `HCaptchaConfig.retryPredicate` option
142 |
143 | # 3.3.7
144 |
145 | - Bugfix: handle Failed to load WebView provider: No WebView installed
146 |
147 | # 3.3.6
148 |
149 | - Bugfix: always dim background if checkbox is visible ([#72](https://github.com/hCaptcha/hcaptcha-android-sdk/issues/72))
150 |
151 | # 3.3.5
152 |
153 | - Show loading screen until the challenge is open when size is `HCaptchaSize.INVISIBLE`
154 |
155 | # 3.3.4
156 |
157 | - Rename `ic_logo` drawable to avoid possible collisions with a host app's drawables
158 | - Prevent closing hCaptcha view on loading container click
159 |
160 | # 3.3.3
161 |
162 | - Fix Android 10 WebView crash on onCheckIsTextEditor call
163 |
164 | # 3.3.2
165 |
166 | - Add `HCaptchaConfig.diagnosticLog` to log diagnostics that are helpful during troubleshooting
167 |
168 | # 3.3.1
169 |
170 | - Fix dialog dismiss crash in specific scenario
171 |
172 | # 3.3.0
173 |
174 | - Disabled cleartext traffic (`android:usesCleartextTraffic="false"` added to `AndroidManifest.xml`)
175 | - `hcaptcha-form.html` asset moved into a variable
176 |
177 | # 3.2.0
178 |
179 | - Add `TOKEN_TIMEOUT` error triggered after a certain configured number of seconds elapsed from the token issuance.
180 |
181 | # 3.1.2
182 |
183 | - Fix checkbox view not dismissible
184 |
185 | # 3.1.1
186 |
187 | - Fix double close error reporting
188 |
189 | # 3.1.0
190 |
191 | - Add `pmd`, `checkstyle`, `spotbugs` tools to build system ([#40](https://github.com/hCaptcha/hcaptcha-android-sdk/issues/40))
192 |
193 | # 3.0.0
194 |
195 | - Add new boolean config option `HCaptchaConfig.hideDialog`.
196 | - (breaking change) Change the behavior of `addOnSuccessListener`, `addOnFailureListener` and `addOnOpenListener` methods.
197 | - previously: the callbacks were removed after utilization
198 | - currently: the callbacks are persisted to be reused for future calls on the same client. This allows multiple human verifications using the same client and the same callback.
199 |
200 | # 2.2.0
201 |
202 | - Add new callback `addOnOpenListener`.
203 |
204 | ## 2.1.0
205 |
206 | - Add `HCaptcha.setup` method to improve cold-start time, enable asset caching ([#24](https://github.com/hCaptcha/hcaptcha-android-sdk/issues/24))
207 |
208 | ## 2.0.0
209 | - Add more error codes (see readme for full list)
210 |
211 |
--------------------------------------------------------------------------------