├── sdk ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values-zh │ │ │ │ └── strings.xml │ │ │ ├── values-am │ │ │ │ └── strings.xml │ │ │ ├── values-ko │ │ │ │ └── strings.xml │ │ │ ├── values-ar │ │ │ │ └── strings.xml │ │ │ ├── values-et │ │ │ │ └── strings.xml │ │ │ ├── values-ht │ │ │ │ └── strings.xml │ │ │ ├── values-iw │ │ │ │ └── strings.xml │ │ │ ├── values-ja │ │ │ │ └── strings.xml │ │ │ ├── values-tk │ │ │ │ └── strings.xml │ │ │ ├── values-yo │ │ │ │ └── strings.xml │ │ │ ├── values-bg │ │ │ │ └── strings.xml │ │ │ ├── values-bn │ │ │ │ └── strings.xml │ │ │ ├── values-bs │ │ │ │ └── strings.xml │ │ │ ├── values-ca │ │ │ │ └── strings.xml │ │ │ ├── values-co │ │ │ │ └── strings.xml │ │ │ ├── values-cs │ │ │ │ └── strings.xml │ │ │ ├── values-da │ │ │ │ └── strings.xml │ │ │ ├── values-fa │ │ │ │ └── strings.xml │ │ │ ├── values-hmn │ │ │ │ └── strings.xml │ │ │ ├── values-hr │ │ │ │ └── strings.xml │ │ │ ├── values-ig │ │ │ │ └── strings.xml │ │ │ ├── values-lo │ │ │ │ └── strings.xml │ │ │ ├── values-mt │ │ │ │ └── strings.xml │ │ │ ├── values-ro │ │ │ │ └── strings.xml │ │ │ ├── values-rw │ │ │ │ └── strings.xml │ │ │ ├── values-sk │ │ │ │ └── strings.xml │ │ │ ├── values-sn │ │ │ │ └── strings.xml │ │ │ ├── values-sr │ │ │ │ └── strings.xml │ │ │ ├── values-su │ │ │ │ └── strings.xml │ │ │ ├── values-th │ │ │ │ └── strings.xml │ │ │ ├── values-uk │ │ │ │ └── strings.xml │ │ │ ├── values-ur │ │ │ │ └── strings.xml │ │ │ ├── values-az │ │ │ │ └── strings.xml │ │ │ ├── values-cy │ │ │ │ └── strings.xml │ │ │ ├── values-eo │ │ │ │ └── strings.xml │ │ │ ├── values-es │ │ │ │ └── strings.xml │ │ │ ├── values-eu │ │ │ │ └── strings.xml │ │ │ ├── values-fr │ │ │ │ └── strings.xml │ │ │ ├── values-ga │ │ │ │ └── strings.xml │ │ │ ├── values-gl │ │ │ │ └── strings.xml │ │ │ ├── values-ha │ │ │ │ └── strings.xml │ │ │ ├── values-haw │ │ │ │ └── strings.xml │ │ │ ├── values-hi │ │ │ │ └── strings.xml │ │ │ ├── values-hy │ │ │ │ └── strings.xml │ │ │ ├── values-it │ │ │ │ └── strings.xml │ │ │ ├── values-ji │ │ │ │ └── strings.xml │ │ │ ├── values-ka │ │ │ │ └── strings.xml │ │ │ ├── values-kk │ │ │ │ └── strings.xml │ │ │ ├── values-km │ │ │ │ └── strings.xml │ │ │ ├── values-ku │ │ │ │ └── strings.xml │ │ │ ├── values-la │ │ │ │ └── strings.xml │ │ │ ├── values-lt │ │ │ │ └── strings.xml │ │ │ ├── values-lv │ │ │ │ └── strings.xml │ │ │ ├── values-mi │ │ │ │ └── strings.xml │ │ │ ├── values-mk │ │ │ │ └── strings.xml │ │ │ ├── values-mn │ │ │ │ └── strings.xml │ │ │ ├── values-no │ │ │ │ └── strings.xml │ │ │ ├── values-or │ │ │ │ └── strings.xml │ │ │ ├── values-pa │ │ │ │ └── strings.xml │ │ │ ├── values-ps │ │ │ │ └── strings.xml │ │ │ ├── values-pt │ │ │ │ └── strings.xml │ │ │ ├── values-sd │ │ │ │ └── strings.xml │ │ │ ├── values-sl │ │ │ │ └── strings.xml │ │ │ ├── values-sm │ │ │ │ └── strings.xml │ │ │ ├── values-st │ │ │ │ └── strings.xml │ │ │ ├── values-sv │ │ │ │ └── strings.xml │ │ │ ├── values-tr │ │ │ │ └── strings.xml │ │ │ ├── values-vi │ │ │ │ └── strings.xml │ │ │ ├── values-xh │ │ │ │ └── strings.xml │ │ │ ├── values-af │ │ │ │ └── strings.xml │ │ │ ├── values-be │ │ │ │ └── strings.xml │ │ │ ├── values-ceb │ │ │ │ └── strings.xml │ │ │ ├── values-de │ │ │ │ └── strings.xml │ │ │ ├── values-el │ │ │ │ └── strings.xml │ │ │ ├── values-fi │ │ │ │ └── strings.xml │ │ │ ├── values-fy │ │ │ │ └── strings.xml │ │ │ ├── values-gd │ │ │ │ └── strings.xml │ │ │ ├── values-gu │ │ │ │ └── strings.xml │ │ │ ├── values-hu │ │ │ │ └── strings.xml │ │ │ ├── values-is │ │ │ │ └── strings.xml │ │ │ ├── values-ky │ │ │ │ └── strings.xml │ │ │ ├── values-lb │ │ │ │ └── strings.xml │ │ │ ├── values-mg │ │ │ │ └── strings.xml │ │ │ ├── values-mr │ │ │ │ └── strings.xml │ │ │ ├── values-ms │ │ │ │ └── strings.xml │ │ │ ├── values-my │ │ │ │ └── strings.xml │ │ │ ├── values-nl │ │ │ │ └── strings.xml │ │ │ ├── values-ny │ │ │ │ └── strings.xml │ │ │ ├── values-pl │ │ │ │ └── strings.xml │ │ │ ├── values-ru │ │ │ │ └── strings.xml │ │ │ ├── values-si │ │ │ │ └── strings.xml │ │ │ ├── values-so │ │ │ │ └── strings.xml │ │ │ ├── values-sq │ │ │ │ └── strings.xml │ │ │ ├── values-sw │ │ │ │ └── strings.xml │ │ │ ├── values-te │ │ │ │ └── strings.xml │ │ │ ├── values-tg │ │ │ │ └── strings.xml │ │ │ ├── values-tl │ │ │ │ └── strings.xml │ │ │ ├── values-tt │ │ │ │ └── strings.xml │ │ │ ├── values-uz │ │ │ │ └── strings.xml │ │ │ ├── values-zu │ │ │ │ └── strings.xml │ │ │ ├── values-in │ │ │ │ └── strings.xml │ │ │ ├── values-jw │ │ │ │ └── strings.xml │ │ │ ├── values-kn │ │ │ │ └── strings.xml │ │ │ ├── values-ml │ │ │ │ └── strings.xml │ │ │ ├── values-ne │ │ │ │ └── strings.xml │ │ │ ├── values-ta │ │ │ │ └── strings.xml │ │ │ ├── values-ug │ │ │ │ └── strings.xml │ │ │ ├── values │ │ │ │ ├── public.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ └── layout │ │ │ │ └── hcaptcha_fragment.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── hcaptcha │ │ │ │ └── sdk │ │ │ │ ├── IHCaptchaHtmlProvider.java │ │ │ │ ├── tasks │ │ │ │ ├── OnOpenListener.java │ │ │ │ ├── OnLoadedListener.java │ │ │ │ ├── OnSuccessListener.java │ │ │ │ └── OnFailureListener.java │ │ │ │ ├── HCaptchaInternalConfig.java │ │ │ │ ├── IHCaptchaRetryPredicate.java │ │ │ │ ├── HCaptchaStateListener.java │ │ │ │ ├── HCaptchaTokenResponse.java │ │ │ │ ├── HCaptchaLog.java │ │ │ │ ├── HCaptchaOrientation.java │ │ │ │ ├── HCaptchaTheme.java │ │ │ │ ├── HCaptchaSize.java │ │ │ │ ├── IHCaptchaVerifier.java │ │ │ │ ├── HCaptchaVerifyParams.java │ │ │ │ ├── HCaptchaException.java │ │ │ │ ├── HCaptchaWebView.java │ │ │ │ ├── HCaptchaCompat.java │ │ │ │ ├── HCaptchaJSInterface.java │ │ │ │ ├── HCaptchaError.java │ │ │ │ ├── HCaptchaHeadlessWebView.java │ │ │ │ └── IHCaptcha.java │ │ ├── AndroidManifest.xml │ │ └── html │ │ │ └── HCaptchaHtml.java.tml │ └── test │ │ └── java │ │ └── com │ │ └── hcaptcha │ │ └── sdk │ │ ├── HCaptchaHtmlTest.java │ │ ├── HCaptchaExceptionTest.java │ │ ├── HCaptchaThemeTest.java │ │ ├── HCaptchaSizeTest.java │ │ ├── HCaptchaDestroyTest.java │ │ ├── HCaptchaErrorTest.java │ │ ├── tasks │ │ └── TaskTest.java │ │ ├── HCaptchaVerifyParamsTest.java │ │ └── HCaptchaWebViewHelperTest.java ├── proguard-rules.pro ├── consumer-rules.pro └── build.gradle ├── benchmark ├── .gitignore ├── src │ ├── main │ │ └── AndroidManifest.xml │ └── androidTest │ │ ├── java │ │ └── com │ │ │ └── hcaptcha │ │ │ └── sdk │ │ │ ├── TestActivity.java │ │ │ ├── TestHCaptchaStateListener.java │ │ │ ├── TestHCaptchaVerifier.java │ │ │ ├── TestHCaptchaHtml.java │ │ │ ├── HCaptchaDebugInfoTest.java │ │ │ ├── HCaptchaWebViewHelperTest.java │ │ │ └── HCaptchaBenchmarkTest.java │ │ └── AndroidManifest.xml ├── benchmark-proguard-rules.pro └── build.gradle ├── compose-sdk ├── .gitignore ├── src │ ├── test │ │ └── .keep │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── hcaptcha │ │ └── sdk │ │ ├── HCaptchaResponse.kt │ │ ├── HCaptchaComposeVerifier.kt │ │ └── HCaptchaCompose.kt ├── consumer-rules.pro ├── proguard-rules.pro └── build.gradle ├── example-app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── values │ │ │ │ ├── dimens.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── hcaptcha │ │ │ │ └── example │ │ │ │ ├── App.java │ │ │ │ └── StartActivity.java │ │ └── AndroidManifest.xml │ └── androidTest │ │ └── java │ │ └── com │ │ └── hcaptcha │ │ └── example │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── test ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ └── strings.xml │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── hcaptcha │ │ │ │ └── sdk │ │ │ │ └── test │ │ │ │ ├── TestActivity.java │ │ │ │ └── TestNonFragmentActivity.java │ │ └── AndroidManifest.xml │ └── androidTest │ │ └── java │ │ └── com │ │ └── hcaptcha │ │ └── sdk │ │ ├── HCaptchaStateTestAdapter.java │ │ ├── HCaptchaConfigTest.java │ │ ├── HCaptchaComposeVerifierTest.kt │ │ ├── HCaptchaTestHtml.java │ │ └── HCaptchaTest.java ├── test-proguard-rules.pro └── build.gradle ├── .jshintrc ├── jitpack.yml ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── config │ ├── pmd.xml │ ├── findbugs-exclude.xml │ └── cve.xml └── shared │ ├── size-check.gradle │ ├── html-java-gen.gradle │ └── code-quality.gradle ├── example-compose-app ├── src │ └── main │ │ ├── res │ │ ├── values │ │ │ └── strings.xml │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ └── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── hcaptcha │ │ └── example │ │ └── compose │ │ └── ComposeActivity.kt └── build.gradle ├── assets └── hcaptcha-invisible-example.gif ├── settings.gradle ├── lombok.config ├── .gitignore ├── LICENSE ├── gradle.properties ├── .github └── actions │ ├── android-benchmark-diff │ ├── action.yml │ └── cli.js │ ├── check-user-permission │ └── action.yml │ └── android-emulator-run │ └── action.yml ├── gradlew.bat ├── MAINTAINERS.md └── CHANGES.md /sdk/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /compose-sdk/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /compose-sdk/src/test/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example-app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /compose-sdk/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 5 3 | } 4 | -------------------------------------------------------------------------------- /benchmark/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - sdk install java 17.0.9-tem 3 | - sdk use java 17.0.9-tem 4 | -------------------------------------------------------------------------------- /test/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | test 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-zh/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 验证你是人类 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-am/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ሰው መሆንዎን ማረጋገጥ 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ko/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 당신이 인간임을 확인 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ar/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | التحقق من أنك إنسان 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-et/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Inimese kinnitamine 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ht/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Verifye ou se moun 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-iw/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | מאמת שאתה אנושי 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ja/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | あなたが人間であることを確認する 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-tk/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Adamdygyňy barlamak 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-yo/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Wiwo pe o jẹ eniyan 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /sdk/src/main/res/values-bg/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Проверка, че сте човек 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-bn/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | আপনাকে যাচাই করা মানুষ 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-bs/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Potvrda da ste čovjek 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ca/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Verificant que sou humà 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-co/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Verificà chì sì umanu 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-cs/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Ověření, že jste člověk 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-da/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Bekræfter du er menneske 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-fa/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | تأیید شما انسان بودن است 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-hmn/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Xam tias koj yog neeg 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-hr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Potvrda da ste čovjek 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ig/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Inyochaa na ị bụ mmadụ 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-lo/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ການພິສູດວ່າທ່ານເປັນມະນຸດ 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-mt/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Tivverifika li int uman 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ro/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Verificăm dacă ești om 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-rw/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Kugenzura ko uri umuntu 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-sk/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Overuje si, či si človek 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-sn/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Kuongorora iwe uri munhu 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-sr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Потврда да сте човек 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-su/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Verifikasi anjeun manusa 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-th/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ยืนยันว่าคุณเป็นมนุษย์ 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-uk/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Перевірка своєї людини 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ur/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | تصدیق کرنا آپ انسان ہیں 3 | -------------------------------------------------------------------------------- /example-compose-app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | hCaptcha Compose 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-az/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | İnsan olduğunuzu təsdiqləmək 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-cy/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Gwirio eich bod yn ddynol 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-eo/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Konfirmante, ke vi estas homa 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Verificando que eres humano 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-eu/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Verificando que eres humano 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-fr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Vérifier que vous êtes humain 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ga/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Ag deimhniú gur duine thú 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-gl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Verificando que eres humano 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ha/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Tabbatar da cewa kai mutum ne 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-haw/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Ke hōʻoia nei he kanaka ʻoe 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-hi/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | सत्यापित करना आप मानव हैं 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-hy/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Հաստատելով, որ դուք մարդ եք 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-it/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Verificare che tu sia umano 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ji/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | באַשטעטיקן איר זענט מענטשלעך 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ka/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | დასტურდება, რომ ადამიანი ხარ 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-kk/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Сіздің адам екеніңізді растау 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-km/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ការផ្ទៀងផ្ទាត់ថាអ្នកជាមនុស្ស 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ku/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | İnsan olduğunuzu doğrulamak 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-la/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Verificare che tu sia umano 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-lt/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Patvirtinti, kad esi žmogus 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-lv/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Pārbaudīt, vai esat cilvēks 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-mi/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Te whakaatu he tangata koe 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-mk/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Потврдување дека сте човек 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-mn/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Таныг хүн гэдгээ баталж байна 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-no/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Bekreft at du er menneske 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-or/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ତୁମେ ମଣିଷ ବୋଲି ଯାଞ୍ଚ କର | 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-pa/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ਤੁਹਾਨੂੰ ਤਸਦੀਕ ਕਰਨਾ ਮਨੁੱਖ ਹੈ 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ps/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | تاسو تصدیق کول انسانان دي 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-pt/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Verificando se você é humano 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-sd/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | تصديق ڪندي توهان انسان آهيو 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-sl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Preverjanje, da ste človek 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-sm/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Faamautinoa o oe o tagata 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-st/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Ho u netefatsa hore u motho 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-sv/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Bekräfta att du är människa 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-tr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | İnsan olduğunuzu doğrulamak 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-vi/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Xác minh bạn là con người 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-xh/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Ukuqinisekisa ukuba ungumntu 3 | -------------------------------------------------------------------------------- /assets/hcaptcha-invisible-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/assets/hcaptcha-invisible-example.gif -------------------------------------------------------------------------------- /sdk/src/main/res/values-af/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Om te verifieer dat jy \'n mens is 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-be/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Пацверджанне, што вы чалавек 3 | 4 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ceb/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Pagpanghimatuud nga ikaw tawo 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Überprüfen, ob Sie ein Mensch sind 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-el/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Η επαλήθευση ότι είστε άνθρωπος 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-fi/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Todentaminen, että olet ihminen 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-fy/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Ferifiearje dat jo minske binne 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-gd/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | A ’dearbhadh gu bheil thu daonna 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-gu/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | તમે માનવ છો તે ચકાસી રહ્યા છીએ 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-hu/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Annak igazolása, hogy ember vagy 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-is/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Staðfestir að þú sért mannlegur 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ky/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Сиздин адам экениңизди тастыктоо 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-lb/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Überprüfen, ob Sie ein Mensch sind 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-mg/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Manamarina fa olombelona ianao 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-mr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | आपण मानव आहात हे सत्यापित करणे 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ms/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Mengesahkan anda adalah manusia 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-my/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | သင်လူသားဖြစ်ကြောင်းအတည်ပြုခြင်း 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-nl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Verifiëren dat u een mens bent 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ny/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Kutsimikizira kuti ndinu munthu 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-pl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Weryfikuję, że jesteś człowiekiem 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ru/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Подтверждение того, что вы человек 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-si/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ඔබ මනුෂ්‍යයෙක් බව තහවුරු කිරීම 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-so/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Hubinta inaad tahay bini aadam 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-sq/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Duke verifikuar se jeni njerëzor 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-sw/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Kukuhakikishia wewe ni mwanadamu 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-te/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | మీరు మనుషులు అని ధృవీకరిస్తున్నారు 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-tg/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Одам будани шуморо тасдиқ кунед 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-tl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Ang pagpapatunay na ikaw ay tao 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-tt/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Сезнең кеше булуыгызны тикшерү 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-uz/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Odam ekanligingizni tasdiqlash 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-zu/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Ukuqinisekisa ukuthi ungumuntu 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-in/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Memverifikasi bahwa Anda adalah manusia 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-jw/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Verifikasi manawa sampeyan manungsa 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-kn/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ನೀವು ಮನುಷ್ಯರೆಂದು ಪರಿಶೀಲಿಸಲಾಗುತ್ತಿದೆ 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ml/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | നിങ്ങൾ മനുഷ്യരാണെന്ന് സ്ഥിരീകരിക്കുന്നു 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ne/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | तपाई मानव हुनुहुन्छ भनेर प्रमाणित गर्दै 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ta/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | நீங்கள் மனிதர் என்பதை சரிபார்க்கிறது 3 | -------------------------------------------------------------------------------- /sdk/src/main/res/values-ug/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | سىزنىڭ ئادەم ئىكەنلىكىڭىزنى دەلىللەش 3 | -------------------------------------------------------------------------------- /test/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/test/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /compose-sdk/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /example-app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example-app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example-app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example-app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /test/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/test/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example-app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 5 | -------------------------------------------------------------------------------- /sdk/src/main/res/values/public.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /example-app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example-app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example-app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /test/src/main/java/com/hcaptcha/sdk/test/TestActivity.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk.test; 2 | 3 | public class TestActivity extends androidx.appcompat.app.AppCompatActivity { 4 | } -------------------------------------------------------------------------------- /test/src/main/java/com/hcaptcha/sdk/test/TestNonFragmentActivity.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk.test; 2 | 3 | public class TestNonFragmentActivity extends android.app.Activity { 4 | } -------------------------------------------------------------------------------- /example-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example-compose-app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-compose-app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /example-compose-app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-compose-app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /example-compose-app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-compose-app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /example-compose-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-compose-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /example-compose-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-compose-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /test/test-proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Proguard rules that are applied to your test apk/code. 2 | -keep class androidx.compose.ui.test.** { *; } 3 | -keep class androidx.compose.ui.platform.** { *; } 4 | -------------------------------------------------------------------------------- /example-compose-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-compose-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /example-compose-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-compose-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /example-compose-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-compose-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /example-compose-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-compose-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /example-compose-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcaptcha/hcaptcha-android-sdk/main/example-compose-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /sdk/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | hcaptcha logo 3 | Verifying you are human 4 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "hcaptcha-android-sdk" 2 | include ':sdk' 3 | include ':test' 4 | include ':benchmark' 5 | include ':example-app' 6 | include ':compose-sdk' 7 | include ':example-compose-app' 8 | -------------------------------------------------------------------------------- /benchmark/src/androidTest/java/com/hcaptcha/sdk/TestActivity.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | 5 | public class TestActivity extends AppCompatActivity { 6 | } 7 | -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | config.stopBubbling = true 2 | lombok.addLombokGeneratedAnnotation = true 3 | lombok.extern.findbugs.addSuppressFBWarnings = true 4 | lombok.addNullAnnotations = findbugs 5 | lombok.addSuppressWarnings = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /sdk/gradle.properties 5 | /.idea 6 | .DS_Store 7 | build 8 | /captures 9 | .externalNativeBuild 10 | .cxx 11 | node_modules 12 | example-app/local.properties 13 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /example-app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/IHCaptchaHtmlProvider.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import lombok.NonNull; 4 | 5 | import java.io.Serializable; 6 | 7 | public interface IHCaptchaHtmlProvider extends Serializable { 8 | 9 | @NonNull 10 | String getHtml(); 11 | } 12 | -------------------------------------------------------------------------------- /example-app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | -------------------------------------------------------------------------------- /sdk/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /example-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/tasks/OnOpenListener.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk.tasks; 2 | 3 | /** 4 | * A hCaptcha open listener class 5 | */ 6 | public interface OnOpenListener { 7 | 8 | /** 9 | * Called when the hCaptcha visual challenge is displayed on the html page 10 | */ 11 | void onOpen(); 12 | } 13 | -------------------------------------------------------------------------------- /example-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/tasks/OnLoadedListener.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk.tasks; 2 | 3 | 4 | /** 5 | * A hCaptcha loader listener class 6 | */ 7 | public interface OnLoadedListener { 8 | 9 | /** 10 | * Called when the hCaptcha challenge is loaded to the html page 11 | */ 12 | void onLoaded(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /sdk/src/test/java/com/hcaptcha/sdk/HCaptchaHtmlTest.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import static org.junit.Assert.assertFalse; 4 | 5 | import org.junit.Test; 6 | 7 | public class HCaptchaHtmlTest { 8 | 9 | @Test 10 | public void html_not_empty() { 11 | assertFalse(new HCaptchaHtml().getHtml().isEmpty()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /sdk/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # Keep all sdk classes 9 | -keep class com.hcaptcha.sdk.** { *; } 10 | -------------------------------------------------------------------------------- /sdk/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /benchmark/benchmark-proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -dontobfuscate 2 | 3 | -ignorewarnings 4 | 5 | -keepattributes *Annotation* 6 | 7 | -dontnote junit.framework.** 8 | -dontnote junit.runner.** 9 | 10 | -dontwarn androidx.test.** 11 | -dontwarn org.junit.** 12 | -dontwarn org.hamcrest.** 13 | -dontwarn com.squareup.javawriter.JavaWriter 14 | 15 | -keepclasseswithmembers @org.junit.runner.RunWith public class * -------------------------------------------------------------------------------- /compose-sdk/src/main/java/com/hcaptcha/sdk/HCaptchaResponse.kt: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk 2 | 3 | enum class HCaptchaEvent { 4 | Loaded, 5 | Opened 6 | } 7 | sealed class HCaptchaResponse { 8 | data class Success(val token: String) : HCaptchaResponse() 9 | data class Failure(val error: HCaptchaError) : HCaptchaResponse() 10 | data class Event(val event: HCaptchaEvent) : HCaptchaResponse() 11 | } 12 | -------------------------------------------------------------------------------- /example-compose-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /example-compose-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/tasks/OnSuccessListener.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk.tasks; 2 | 3 | 4 | /** 5 | * A success listener class 6 | * 7 | * @param expected result type 8 | */ 9 | public interface OnSuccessListener { 10 | 11 | /** 12 | * Called when the challenge is successfully completed 13 | * 14 | * @param result the hCaptcha token result 15 | */ 16 | void onSuccess(R result); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/tasks/OnFailureListener.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk.tasks; 2 | 3 | import com.hcaptcha.sdk.HCaptchaException; 4 | 5 | 6 | /** 7 | * A failure listener class 8 | */ 9 | public interface OnFailureListener { 10 | 11 | /** 12 | * Called whenever there is a hCaptcha error or user closed the challenge dialog 13 | * 14 | * @param exception the exception 15 | */ 16 | void onFailure(HCaptchaException exception); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/HCaptchaInternalConfig.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * hCaptcha internal config keep internal configuration, which should not be accessible by end user 10 | */ 11 | @Data 12 | @Builder(toBuilder = true) 13 | class HCaptchaInternalConfig implements Serializable { 14 | 15 | /** 16 | * HTML Provider 17 | */ 18 | @Builder.Default 19 | private IHCaptchaHtmlProvider htmlProvider = new HCaptchaHtml(); 20 | } 21 | -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/IHCaptchaRetryPredicate.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * On failure retry decider class 7 | */ 8 | @FunctionalInterface 9 | public interface IHCaptchaRetryPredicate extends Serializable { 10 | 11 | /** 12 | * Allows retrying in case of verification errors. 13 | * 14 | * @param config the hCaptcha config 15 | * @param exception the hCaptcha exception 16 | */ 17 | boolean shouldRetry(HCaptchaConfig config, HCaptchaException exception); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /benchmark/src/androidTest/java/com/hcaptcha/sdk/TestHCaptchaStateListener.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | public class TestHCaptchaStateListener extends HCaptchaStateListener { 4 | @Override 5 | void onSuccess(String token) { 6 | // no implementation need for performance measurement 7 | } 8 | 9 | @Override 10 | void onFailure(HCaptchaException exception) { 11 | // no implementation need for performance measurement 12 | } 13 | 14 | @Override 15 | void onOpen() { 16 | // no implementation need for performance measurement 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/HCaptchaStateListener.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | abstract class HCaptchaStateListener implements Parcelable { 7 | 8 | abstract void onSuccess(String token); 9 | 10 | abstract void onFailure(HCaptchaException exception); 11 | 12 | abstract void onOpen(); 13 | 14 | @Override 15 | public int describeContents() { 16 | return 0; 17 | } 18 | 19 | @Override 20 | public void writeToParcel(Parcel dest, int flags) { 21 | // nothing to persist 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaStateTestAdapter.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import static com.hcaptcha.sdk.AssertUtil.failAsNonReachable; 4 | 5 | public class HCaptchaStateTestAdapter extends HCaptchaStateListener { 6 | @Override 7 | void onOpen() { 8 | // empty default implementation to reduce amount of boilerplate code in tests 9 | } 10 | 11 | @Override 12 | void onSuccess(String response) { 13 | failAsNonReachable(); 14 | } 15 | 16 | @Override 17 | void onFailure(HCaptchaException exception) { 18 | failAsNonReachable(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sdk/src/main/html/HCaptchaHtml.java.tml: -------------------------------------------------------------------------------- 1 | // Auto-generated. Don't edit it directly. 2 | // Modify sdk/src/main/html/hcaptcha.html instead and run `./gradlew build` instead 3 | 4 | package ${packageName}; 5 | 6 | import lombok.NonNull; 7 | 8 | final class HCaptchaHtml implements IHCaptchaHtmlProvider { 9 | 10 | @Override 11 | @NonNull 12 | @SuppressWarnings("PMD.AvoidDuplicateLiterals") 13 | public String getHtml() { 14 | return ${htmlContent}; 15 | } 16 | 17 | @SuppressWarnings("java:S106") 18 | public static void main(String[] args) { 19 | System.out.println(new HCaptchaHtml().getHtml()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example-app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | hCaptcha Example 3 | Reset 4 | Setup 5 | Verify 6 | Mark used 7 | Destroy 8 | Loading 9 | Web Debug 10 | Disable HW Accel 11 | Hide Dialog 12 | Hit test 13 | Dark Theme 14 | 15 | -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/HCaptchaTokenResponse.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import android.os.Handler; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | 8 | /** 9 | * Token response which contains the token string to be verified 10 | */ 11 | @AllArgsConstructor 12 | public class HCaptchaTokenResponse { 13 | 14 | @Getter 15 | private final String tokenResult; 16 | 17 | private final Handler handler; 18 | 19 | /** 20 | * This method will signal SDK to not fire {@link HCaptchaError#TOKEN_TIMEOUT} 21 | */ 22 | public void markUsed() { 23 | handler.removeCallbacksAndMessages(null); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /benchmark/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /sdk/src/test/java/com/hcaptcha/sdk/HCaptchaExceptionTest.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.Test; 6 | 7 | public class HCaptchaExceptionTest { 8 | 9 | @Test 10 | public void exception_matches_error() { 11 | final HCaptchaError error = HCaptchaError.NETWORK_ERROR; 12 | try { 13 | throw new HCaptchaException(error); 14 | } catch (HCaptchaException hCaptchaException1) { 15 | assertEquals(error.getErrorId(), hCaptchaException1.getStatusCode()); 16 | assertEquals(error.getMessage(), hCaptchaException1.getMessage()); 17 | assertEquals(error, hCaptchaException1.getHCaptchaError()); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sdk/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -dontwarn javax.annotation.processing.** 2 | -dontwarn lombok.core.configuration.** 3 | -dontwarn org.apache.tools.** 4 | -dontwarn android.content.pm.PackageManager$ApplicationInfoFlags 5 | -dontwarn edu.umd.cs.findbugs.annotations.* 6 | -dontwarn java.beans.* 7 | -dontwarn lombok.* 8 | 9 | # Prevent obfuscating the names when serializing to JSON 10 | -keep class com.hcaptcha.sdk.HCaptchaConfig { *; } 11 | -keep class com.hcaptcha.sdk.HCaptchaVerifyParams { *; } 12 | -keepclasseswithmembernames public enum com.hcaptcha.sdk.** { 13 | @com.fasterxml.jackson.annotation.JsonValue *; 14 | } 15 | 16 | # Remove debug logging from the production code 17 | -assumenosideeffects class com.hcaptcha.sdk.HCaptchaLog { 18 | public static void *(...); 19 | } 20 | -------------------------------------------------------------------------------- /example-compose-app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaConfigTest.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import static com.hcaptcha.sdk.compose.HCaptchaComposeTest.PASSIVE_SITE_KEY; 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import android.os.Parcel; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import androidx.test.ext.junit.runners.AndroidJUnit4; 12 | 13 | @RunWith(AndroidJUnit4.class) 14 | public class HCaptchaConfigTest { 15 | 16 | @Test 17 | public void testParcelable() { 18 | HCaptchaConfig config = HCaptchaConfig.builder() 19 | .siteKey(PASSIVE_SITE_KEY) 20 | .build(); 21 | Parcel parcel = Parcel.obtain(); 22 | parcel.writeSerializable(config); 23 | assertTrue(parcel.dataSize() > 0); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /compose-sdk/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/HCaptchaLog.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import android.util.Log; 4 | 5 | import java.util.Locale; 6 | 7 | /** 8 | * Logger 9 | */ 10 | public final class HCaptchaLog { 11 | private static final String TAG = "hcaptcha"; 12 | 13 | static boolean sDiagnosticsLogEnabled = false; 14 | 15 | private HCaptchaLog() { 16 | } 17 | 18 | public static void w(String message) { 19 | Log.w(TAG, message); 20 | } 21 | 22 | public static void d(String message) { 23 | if (sDiagnosticsLogEnabled) { 24 | Log.d(TAG, message); 25 | } 26 | } 27 | 28 | public static void d(String message, Object... args) { 29 | if (sDiagnosticsLogEnabled) { 30 | Log.d(TAG, String.format(Locale.getDefault(), message, args)); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example-app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 17 | -------------------------------------------------------------------------------- /example-app/src/main/java/com/hcaptcha/example/App.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.example; 2 | 3 | import android.app.Application; 4 | import android.os.StrictMode; 5 | 6 | public class App extends Application { 7 | @Override 8 | public void onCreate() { 9 | super.onCreate(); 10 | 11 | if (com.hcaptcha.sdk.BuildConfig.DEBUG) { 12 | StrictMode.setThreadPolicy( 13 | new StrictMode.ThreadPolicy.Builder() 14 | .detectAll() 15 | .penaltyLog() 16 | .build() 17 | ); 18 | StrictMode.setVmPolicy( 19 | new StrictMode.VmPolicy.Builder() 20 | .detectAll() 21 | .penaltyLog() 22 | .build() 23 | ); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example-app/src/androidTest/java/com/hcaptcha/example/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.example; 2 | 3 | import android.content.Context; 4 | import androidx.test.platform.app.InstrumentationRegistry; 5 | import androidx.test.ext.junit.runners.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 23 | assertEquals("com.hcaptcha.example", appContext.getPackageName()); 24 | } 25 | } -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/HCaptchaOrientation.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.fasterxml.jackson.annotation.JsonValue; 6 | 7 | import java.io.Serializable; 8 | 9 | 10 | /** 11 | * The hCaptcha challenge orientation 12 | */ 13 | public enum HCaptchaOrientation implements Serializable { 14 | 15 | PORTRAIT("portrait"), 16 | 17 | LANDSCAPE("landscape"); 18 | 19 | private final String orientation; 20 | 21 | HCaptchaOrientation(final String orientation) { 22 | this.orientation = orientation; 23 | } 24 | 25 | /** 26 | * @return the hCaptcha api.js string encoding 27 | */ 28 | public String getOrientation() { 29 | return this.orientation; 30 | } 31 | 32 | @JsonValue 33 | @NonNull 34 | @Override 35 | public String toString() { 36 | return orientation; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /gradle/config/pmd.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | HCaptcha Android SDK RuleSet 7 | .*/R.java 8 | .*/gen/.* 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /gradle/config/findbugs-exclude.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/HCaptchaTheme.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.fasterxml.jackson.annotation.JsonValue; 6 | 7 | import java.io.Serializable; 8 | 9 | 10 | /** 11 | * The hCaptcha checkbox theme 12 | */ 13 | public enum HCaptchaTheme implements Serializable { 14 | 15 | /** 16 | * Dark theme 17 | */ 18 | DARK("dark"), 19 | 20 | /** 21 | * Light theme 22 | */ 23 | LIGHT("light"), 24 | 25 | /** 26 | * Contrast theme 27 | */ 28 | CONTRAST("contrast"); 29 | 30 | private final String theme; 31 | 32 | HCaptchaTheme(final String theme) { 33 | this.theme = theme; 34 | } 35 | 36 | /** 37 | * @return the hCaptcha api.js string encoding 38 | */ 39 | public String getTheme() { 40 | return this.theme; 41 | } 42 | 43 | @JsonValue 44 | @NonNull 45 | @Override 46 | public String toString() { 47 | return theme; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /sdk/src/test/java/com/hcaptcha/sdk/HCaptchaThemeTest.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import com.fasterxml.jackson.core.JsonProcessingException; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import org.junit.Test; 8 | 9 | public class HCaptchaThemeTest { 10 | 11 | @Test 12 | public void enum_values() { 13 | assertEquals("dark", HCaptchaTheme.DARK.getTheme()); 14 | assertEquals("light", HCaptchaTheme.LIGHT.getTheme()); 15 | assertEquals("contrast", HCaptchaTheme.CONTRAST.getTheme()); 16 | } 17 | 18 | @Test 19 | public void enum_to_string() { 20 | assertEquals(HCaptchaTheme.DARK.getTheme(), HCaptchaTheme.DARK.getTheme()); 21 | } 22 | 23 | @Test 24 | public void serializes_to_json_value() throws JsonProcessingException { 25 | final ObjectMapper objectMapper = new ObjectMapper(); 26 | final String serialized = objectMapper.writeValueAsString(HCaptchaTheme.CONTRAST); 27 | assertEquals("\"contrast\"", serialized); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /sdk/src/test/java/com/hcaptcha/sdk/HCaptchaSizeTest.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import com.fasterxml.jackson.core.JsonProcessingException; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import org.junit.Test; 8 | 9 | public class HCaptchaSizeTest { 10 | 11 | @Test 12 | public void enum_values() { 13 | assertEquals("invisible", HCaptchaSize.INVISIBLE.getSize()); 14 | assertEquals("compact", HCaptchaSize.COMPACT.getSize()); 15 | assertEquals("normal", HCaptchaSize.NORMAL.getSize()); 16 | } 17 | 18 | @Test 19 | public void enum_to_string() { 20 | assertEquals(HCaptchaSize.INVISIBLE.getSize(), HCaptchaSize.INVISIBLE.toString()); 21 | } 22 | 23 | @Test 24 | public void serializes_to_json_value() throws JsonProcessingException { 25 | final ObjectMapper objectMapper = new ObjectMapper(); 26 | final String serialized = objectMapper.writeValueAsString(HCaptchaSize.INVISIBLE); 27 | assertEquals("\"invisible\"", serialized); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/HCaptchaSize.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.fasterxml.jackson.annotation.JsonValue; 6 | 7 | import java.io.Serializable; 8 | 9 | 10 | /** 11 | * The hCaptcha checkbox size 12 | */ 13 | public enum HCaptchaSize implements Serializable { 14 | 15 | /** 16 | * Checkbox is hidden and challenge is automatically displayed. 17 | */ 18 | INVISIBLE("invisible"), 19 | 20 | /** 21 | * Checkbox has a 'normal' size and user must press it to show the challenge. 22 | */ 23 | NORMAL("normal"), 24 | 25 | /** 26 | * Checkbox has a 'compact' size and user must press it to show the challenge. 27 | */ 28 | COMPACT("compact"); 29 | 30 | private final String size; 31 | 32 | HCaptchaSize(final String size) { 33 | this.size = size; 34 | } 35 | 36 | /** 37 | * @return the hCaptcha api.js string encoding 38 | */ 39 | public String getSize() { 40 | return this.size; 41 | } 42 | 43 | @JsonValue 44 | @NonNull 45 | @Override 46 | public String toString() { 47 | return size; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /sdk/src/test/java/com/hcaptcha/sdk/HCaptchaDestroyTest.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import static org.junit.Assert.assertNull; 4 | import static org.mockito.Mockito.mock; 5 | import static org.mockito.Mockito.verify; 6 | 7 | import android.app.Activity; 8 | 9 | import org.junit.Test; 10 | 11 | import java.lang.reflect.Field; 12 | 13 | /** 14 | * Unit test to verify that HCaptcha.destroy() calls underlying verifier.destroy() 15 | * and clears the cached verifier reference. 16 | */ 17 | public class HCaptchaDestroyTest { 18 | 19 | @Test 20 | public void destroy_invokes_verifier_and_clears_reference() throws Exception { 21 | final Activity activity = mock(Activity.class); 22 | final HCaptcha hCaptcha = HCaptcha.getClient(activity); 23 | 24 | // Inject a mocked verifier via reflection 25 | final IHCaptchaVerifier verifier = mock(IHCaptchaVerifier.class); 26 | final Field f = HCaptcha.class.getDeclaredField("captchaVerifier"); 27 | f.setAccessible(true); 28 | f.set(hCaptcha, verifier); 29 | 30 | // Act 31 | hCaptcha.destroy(); 32 | 33 | // Assert 34 | verify(verifier).destroy(); 35 | assertNull(f.get(hCaptcha)); 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/IHCaptchaVerifier.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import android.app.Activity; 4 | import androidx.annotation.Nullable; 5 | 6 | import com.hcaptcha.sdk.tasks.OnFailureListener; 7 | import com.hcaptcha.sdk.tasks.OnLoadedListener; 8 | import com.hcaptcha.sdk.tasks.OnOpenListener; 9 | import com.hcaptcha.sdk.tasks.OnSuccessListener; 10 | import lombok.NonNull; 11 | 12 | interface IHCaptchaVerifier extends 13 | OnLoadedListener, 14 | OnOpenListener, 15 | OnSuccessListener, 16 | OnFailureListener { 17 | 18 | /** 19 | * Starts the human verification process. 20 | * 21 | * @param activity The activity to start verification in 22 | * @param verifyParams Optional verification parameters (phone prefix, phone number, etc.) 23 | */ 24 | void startVerification(@NonNull Activity activity, @Nullable HCaptchaVerifyParams verifyParams); 25 | 26 | /** 27 | * Force stop verification and release resources. 28 | */ 29 | void reset(); 30 | 31 | /** 32 | * Fully destroy underlying WebView and release all resources. 33 | * Unlike {@link #reset()}, this tears down preloaded state as well. 34 | */ 35 | void destroy(); 36 | } 37 | -------------------------------------------------------------------------------- /example-app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /example-app/src/main/java/com/hcaptcha/example/StartActivity.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.example; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.view.Gravity; 6 | import android.widget.Button; 7 | import android.widget.FrameLayout; 8 | 9 | import androidx.appcompat.app.AppCompatActivity; 10 | 11 | public class StartActivity extends AppCompatActivity { 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | 16 | FrameLayout root = new FrameLayout(this); 17 | 18 | Button button = new Button(this); 19 | button.setText("Open hCaptcha"); 20 | 21 | FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( 22 | FrameLayout.LayoutParams.WRAP_CONTENT, 23 | FrameLayout.LayoutParams.WRAP_CONTENT 24 | ); 25 | params.gravity = Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL; 26 | button.setLayoutParams(params); 27 | 28 | button.setOnClickListener(v -> { 29 | Intent intent = new Intent(StartActivity.this, MainActivity.class); 30 | startActivity(intent); 31 | }); 32 | 33 | root.addView(button); 34 | setContentView(root); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /gradle/shared/size-check.gradle: -------------------------------------------------------------------------------- 1 | android.libraryVariants.all { variant -> 2 | def variantName = variant.name.capitalize() 3 | tasks.register("report${variantName}AarSize") { 4 | group 'Help' 5 | description "Report ${variant.name} AAR size" 6 | dependsOn variant.packageLibraryProvider 7 | 8 | doFirst { 9 | var aarPath = variant.packageLibraryProvider.get().archiveFile.get().getAsFile() 10 | long aarSizeKb = aarPath.length() / 1024 11 | println("File ${aarPath} is ${aarSizeKb}Kbyte") 12 | } 13 | } 14 | 15 | tasks.register("check${variantName}AarSize") { 16 | group 'Verification' 17 | description "Checks ${variant.name} AAR size doesn't exceed ${project.ext}Kb" 18 | dependsOn variant.packageLibraryProvider 19 | 20 | doFirst { 21 | var aarFile = variant.packageLibraryProvider.get().archiveFile.get().getAsFile() 22 | long aarSizeKb = aarFile.length() / 1024 23 | if (aarSizeKb > maxAarSizeKb) { 24 | throw new GradleException("${aarPath} size exceeded! ${aarSizeKb}Kbyte > ${MAX_AAR_SIZE_KB}Kbyte") 25 | } 26 | } 27 | } 28 | 29 | tasks.named("check").configure { dependsOn "check${variantName}AarSize" } 30 | } -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/HCaptchaVerifyParams.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | import java.io.Serializable; 11 | 12 | /** 13 | * Parameters supplied at verification time. 14 | */ 15 | @Data 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | @Builder(toBuilder = true) 19 | @JsonInclude(JsonInclude.Include.NON_NULL) 20 | public class HCaptchaVerifyParams implements Serializable { 21 | 22 | /** 23 | * Optional phone country calling code (without '+'), e.g., "44". 24 | * Used in MFA flows. 25 | */ 26 | @JsonProperty("mfa_phoneprefix") 27 | private String phonePrefix; 28 | 29 | /** 30 | * Optional full phone number in E.164 format ("+44123..."), for use in MFA. 31 | */ 32 | @JsonProperty("mfa_phone") 33 | private String phoneNumber; 34 | 35 | /** 36 | * Optional request data string to be passed to hCaptcha. 37 | * When provided, JS will call hcaptcha.setData({rqdata: value}) if available. 38 | */ 39 | @JsonProperty("rqdata") 40 | private String rqdata; 41 | } 42 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # To test more aggressive optimizations 21 | android.enableR8.fullMode=true 22 | # Kotline version 23 | kotlin_version=1.9.10 24 | compose_version=1.5.3 -------------------------------------------------------------------------------- /benchmark/src/androidTest/java/com/hcaptcha/sdk/TestHCaptchaVerifier.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import android.app.Activity; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.annotation.Nullable; 7 | 8 | public class TestHCaptchaVerifier implements IHCaptchaVerifier { 9 | 10 | @Override 11 | public void startVerification(@NonNull Activity activity, @Nullable HCaptchaVerifyParams verifyParams) { 12 | // no implementation need for performance measurement 13 | } 14 | 15 | @Override 16 | public void onFailure(HCaptchaException exception) { 17 | // no implementation need for performance measurement 18 | } 19 | 20 | @Override 21 | public void onLoaded() { 22 | // no implementation need for performance measurement 23 | } 24 | 25 | @Override 26 | public void onOpen() { 27 | // no implementation need for performance measurement 28 | } 29 | 30 | @Override 31 | public void onSuccess(String s) { 32 | // no implementation need for performance measurement 33 | } 34 | 35 | @Override 36 | public void reset() { 37 | // no implementation need for performance measurement 38 | } 39 | 40 | @Override 41 | public void destroy() { 42 | // no implementation need for performance measurement 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/HCaptchaException.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.NonNull; 9 | 10 | 11 | /** 12 | * A checked exception which contains an {@link HCaptchaError} id and message. 13 | */ 14 | @Data 15 | @AllArgsConstructor 16 | @EqualsAndHashCode(callSuper = true) 17 | public class HCaptchaException extends Exception { 18 | 19 | /** 20 | * The hCaptcha error object 21 | */ 22 | @NonNull 23 | private final HCaptchaError hCaptchaError; 24 | 25 | @Nullable 26 | private final String message; 27 | 28 | public HCaptchaException(HCaptchaError error) { 29 | this(error, null); 30 | } 31 | 32 | /** 33 | * @return The {@link HCaptchaError} error 34 | */ 35 | public HCaptchaError getHCaptchaError() { 36 | return hCaptchaError; 37 | } 38 | 39 | /** 40 | * @return The error id 41 | */ 42 | public int getStatusCode() { 43 | return hCaptchaError.getErrorId(); 44 | } 45 | 46 | /** 47 | * @return The message string 48 | */ 49 | @Nullable 50 | @Override 51 | public String getMessage() { 52 | return message == null ? hCaptchaError.getMessage() : message; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /example-compose-app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /benchmark/src/androidTest/java/com/hcaptcha/sdk/TestHCaptchaHtml.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | class TestHCaptchaHtml implements IHCaptchaHtmlProvider { 6 | 7 | @Override 8 | @NonNull 9 | public String getHtml() { 10 | return "\n" 11 | + "\n" 12 | + " \n" 13 | + "\n" 14 | + "\n" 15 | + "
\n" 16 | + " \n" 26 | + "\n" 27 | + "\n"; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/actions/android-benchmark-diff/action.yml: -------------------------------------------------------------------------------- 1 | name: android-benchmark-diff 2 | description: Do diff against previous/reference benchmarkData.json and prepare Markdown table with results 3 | inputs: 4 | reference: 5 | description: Path to the reference benchmarkData.json 6 | required: true 7 | compare-with: 8 | description: Path to a benchmarkData.json that will be compared with the reference 9 | required: true 10 | reference-cache-key: 11 | description: Key name to cache the reference benchmarkData.json 12 | required: true 13 | default: reference-json-key 14 | outputs: 15 | markdown-table: 16 | description: Human readable table of results 17 | value: ${{ steps.diff.outputs.markdown-table }} 18 | runs: 19 | using: "composite" 20 | steps: 21 | - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 22 | with: 23 | path: ${{ inputs.reference }} 24 | key: ${{ inputs.reference-cache-key }} 25 | restore-keys: ${{ inputs.reference-cache-key }} 26 | - id: diff 27 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 28 | with: 29 | script: | 30 | const cli = require('${{ github.workspace }}/.github/actions/android-benchmark-diff/cli.js') 31 | const { report } = cli 32 | const table = report('${{ inputs.reference }}', '${{ inputs.compare-with }}', true) 33 | core.setOutput('markdown-table', table); 34 | -------------------------------------------------------------------------------- /benchmark/src/androidTest/java/com/hcaptcha/sdk/HCaptchaDebugInfoTest.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.benchmark.BenchmarkState; 6 | import androidx.benchmark.junit4.BenchmarkRule; 7 | import androidx.test.ext.junit.runners.AndroidJUnit4; 8 | import androidx.test.platform.app.InstrumentationRegistry; 9 | 10 | import org.junit.Rule; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | 14 | @RunWith(AndroidJUnit4.class) 15 | public class HCaptchaDebugInfoTest { 16 | @Rule 17 | public BenchmarkRule benchmarkRule = new BenchmarkRule(); 18 | 19 | @Test 20 | public void benchmarkDebugInfo() throws Exception { 21 | Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); 22 | final BenchmarkState state = benchmarkRule.getState(); 23 | while (state.keepRunning()) { 24 | new HCaptchaDebugInfo(context).debugInfo( 25 | context.getPackageName(), 26 | context.getPackageCodePath()); 27 | } 28 | } 29 | 30 | @Test 31 | public void benchmarkDebugSys() throws Exception { 32 | Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); 33 | final BenchmarkState state = benchmarkRule.getState(); 34 | while (state.keepRunning()) { 35 | new HCaptchaDebugInfo(context).roBuildProps(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example-app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | -------------------------------------------------------------------------------- /test/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /gradle/shared/html-java-gen.gradle: -------------------------------------------------------------------------------- 1 | android.libraryVariants.all { variant -> 2 | def packageName = android.namespace 3 | def variantName = variant.name.capitalize() 4 | def outputDir = layout.buildDirectory.dir("generated/source/hcaptcha/${variant.name}/${packageName.replaceAll('\\.', '/')}").get().asFile 5 | def generateTask = tasks.register("generate${variantName}JavaClassFromStaticHtml") { 6 | group 'Generate' 7 | description "Generate HTML java class" 8 | 9 | // Declare inputs for proper incremental build support 10 | inputs.file("$projectDir/src/main/html/hcaptcha.html") 11 | inputs.file("$projectDir/src/main/html/HCaptchaHtml.java.tml") 12 | outputs.file("$outputDir/HCaptchaHtml.java") 13 | 14 | doFirst { 15 | def outputJavaClass = file("$outputDir/HCaptchaHtml.java") 16 | def template = file("$projectDir/src/main/html/HCaptchaHtml.java.tml").text 17 | def html = file("$projectDir/src/main/html/hcaptcha.html") 18 | .readLines() 19 | .stream() 20 | .map({l -> "\"${l.replaceAll('"', '\\\\"')}\\n\""}) 21 | .collect(java.util.stream.Collectors.joining("\n${' ' * 16}+ ")) 22 | 23 | def engine = new groovy.text.SimpleTemplateEngine() 24 | def src = engine.createTemplate(template).make([ 25 | "htmlContent": html, 26 | "packageName": packageName 27 | ]) 28 | 29 | outputDir.mkdirs() 30 | outputJavaClass.write(src.toString()) 31 | } 32 | } 33 | 34 | // Ensure the generation task runs before compilation 35 | variant.registerJavaGeneratingTask(generateTask.get(), outputDir) 36 | } -------------------------------------------------------------------------------- /sdk/src/main/res/layout/hcaptcha_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 22 | 23 | 29 | 30 | 37 | 38 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/HCaptchaWebView.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | import android.os.Looper; 6 | import android.util.AttributeSet; 7 | import android.webkit.WebView; 8 | import androidx.annotation.NonNull; 9 | import androidx.annotation.Nullable; 10 | import androidx.annotation.RequiresApi; 11 | 12 | public class HCaptchaWebView extends WebView { 13 | public HCaptchaWebView(@NonNull Context context) { 14 | super(context); 15 | } 16 | 17 | public HCaptchaWebView(@NonNull Context context, @Nullable AttributeSet attrs) { 18 | super(context, attrs); 19 | } 20 | 21 | public HCaptchaWebView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 22 | super(context, attrs, defStyleAttr); 23 | } 24 | 25 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 26 | public HCaptchaWebView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { 27 | super(context, attrs, defStyleAttr, defStyleRes); 28 | } 29 | 30 | /** 31 | * Workaround for crash in WebViewChromium 32 | * Details: 33 | * - stackoverflow 34 | * - github issues 35 | */ 36 | @Override 37 | public boolean onCheckIsTextEditor() { 38 | if (Looper.myLooper() == Looper.getMainLooper()) { 39 | return super.onCheckIsTextEditor(); 40 | } else { 41 | return false; 42 | } 43 | } 44 | 45 | @Override 46 | public boolean performClick() { 47 | return false; 48 | } 49 | 50 | public boolean isDestroyed() { 51 | return getParent() == null; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /example-app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | def intProp(name, fallback) { 6 | return project.hasProperty(name) ? Integer.parseInt(project.getProperty(name)) : fallback 7 | } 8 | 9 | def prop(name, fallback) { 10 | return project.hasProperty(name) ? project.getProperty(name) : fallback 11 | } 12 | 13 | android { 14 | compileSdk intProp("exampleCompileSdkVersion", 35) 15 | namespace 'com.hcaptcha.example' 16 | 17 | defaultConfig { 18 | minSdkVersion 23 19 | targetSdkVersion intProp("exampleTargetSdkVersion", 35) 20 | versionCode 1 21 | versionName "0.0.1" 22 | 23 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 25 | } 26 | 27 | compileOptions { 28 | // Sets Java compatibility to Java 8 29 | sourceCompatibility JavaVersion.VERSION_1_8 30 | targetCompatibility JavaVersion.VERSION_1_8 31 | } 32 | 33 | buildTypes { 34 | release { 35 | signingConfig signingConfigs.debug 36 | minifyEnabled true 37 | } 38 | } 39 | 40 | lint { 41 | disable 'UsingOnClickInXml' 42 | } 43 | } 44 | 45 | dependencies { 46 | implementation project(path: ':sdk') 47 | 48 | //noinspection GradleDependency 49 | implementation "androidx.appcompat:appcompat:${prop('exampleAppcompatVersion', '1.3.1')}" 50 | implementation "com.google.android.flexbox:flexbox:3.0.0" 51 | 52 | if (!prop('disableLeakCanary', 'false').toBoolean()) { 53 | debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14' 54 | } 55 | 56 | testImplementation 'junit:junit:4.13.2' 57 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 58 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 59 | } 60 | -------------------------------------------------------------------------------- /example-compose-app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | def intProp(name, fallback) { 7 | return project.hasProperty(name) ? Integer.parseInt(project.getProperty(name)) : fallback 8 | } 9 | 10 | android { 11 | compileSdk intProp("exampleCompileSdkVersion", 35) 12 | namespace 'com.hcaptcha.example.compose' 13 | 14 | defaultConfig { 15 | minSdkVersion 21 16 | targetSdkVersion intProp("exampleTargetSdkVersion", 35) 17 | versionCode 1 18 | versionName "0.0.1" 19 | 20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt') 22 | } 23 | 24 | kotlinOptions { 25 | jvmTarget = JavaVersion.VERSION_1_8 26 | } 27 | 28 | buildTypes { 29 | release { 30 | signingConfig signingConfigs.debug 31 | minifyEnabled true 32 | } 33 | } 34 | 35 | lint { 36 | disable 'UsingOnClickInXml' 37 | } 38 | 39 | buildFeatures { // Enables Jetpack Compose for this module 40 | compose = true 41 | } 42 | 43 | composeOptions { 44 | kotlinCompilerExtensionVersion = "$compose_version" 45 | } 46 | } 47 | 48 | dependencies { 49 | implementation project(path: ':compose-sdk') 50 | 51 | implementation "androidx.compose.ui:ui:$compose_version" 52 | implementation 'androidx.compose.material3:material3:1.2.1' 53 | implementation 'androidx.activity:activity-ktx:1.8.2' 54 | implementation 'androidx.activity:activity-compose:1.8.2' 55 | implementation "androidx.compose.foundation:foundation-layout-android:$compose_version" 56 | 57 | testImplementation 'junit:junit:4.13.2' 58 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 59 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 60 | } 61 | -------------------------------------------------------------------------------- /sdk/src/test/java/com/hcaptcha/sdk/HCaptchaErrorTest.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertThrows; 5 | 6 | import org.junit.Test; 7 | 8 | public class HCaptchaErrorTest { 9 | 10 | @Test 11 | public void enum_codes() { 12 | assertEquals("No internet connection", HCaptchaError.NETWORK_ERROR.toString()); 13 | assertEquals("Session Timeout", HCaptchaError.SESSION_TIMEOUT.toString()); 14 | assertEquals("Challenge Closed", HCaptchaError.CHALLENGE_CLOSED.toString()); 15 | assertEquals("Rate Limited", HCaptchaError.RATE_LIMITED.toString()); 16 | assertEquals("Unknown error", HCaptchaError.ERROR.toString()); 17 | } 18 | 19 | @Test 20 | @SuppressWarnings("magicnumber") 21 | public void enum_ids() { 22 | assertEquals(7, HCaptchaError.NETWORK_ERROR.getErrorId()); 23 | assertEquals(15, HCaptchaError.SESSION_TIMEOUT.getErrorId()); 24 | assertEquals(30, HCaptchaError.CHALLENGE_CLOSED.getErrorId()); 25 | assertEquals(31, HCaptchaError.RATE_LIMITED.getErrorId()); 26 | assertEquals(29, HCaptchaError.ERROR.getErrorId()); 27 | } 28 | 29 | @Test 30 | @SuppressWarnings("checkstyle:magicnumber") 31 | public void get_enum_from_id() { 32 | assertEquals(HCaptchaError.NETWORK_ERROR, HCaptchaError.fromId(7)); 33 | assertEquals(HCaptchaError.SESSION_TIMEOUT, HCaptchaError.fromId(15)); 34 | assertEquals(HCaptchaError.CHALLENGE_CLOSED, HCaptchaError.fromId(30)); 35 | assertEquals(HCaptchaError.RATE_LIMITED, HCaptchaError.fromId(31)); 36 | assertEquals(HCaptchaError.ERROR, HCaptchaError.fromId(29)); 37 | } 38 | 39 | @Test 40 | @SuppressWarnings("checkstyle:MagicNumber") 41 | public void get_enum_from_invalid_id_throws() { 42 | assertThrows(RuntimeException.class, () -> HCaptchaError.fromId(-999)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/actions/check-user-permission/action.yml: -------------------------------------------------------------------------------- 1 | name: Check User Permission 2 | description: Checks if the user has the required permission level. 3 | 4 | inputs: 5 | token: 6 | description: Secret GitHub API token to use for making API requests. 7 | default: ${{ github.token }} 8 | required: true 9 | require: 10 | description: 'Permission level to check against (admin, write, read)' 11 | default: write 12 | required: true 13 | 14 | outputs: 15 | granted: 16 | description: 'true if the user has the required permission, false otherwise' 17 | value: ${{ steps.check.outputs.granted }} 18 | permission: 19 | description: actual user permission (admin, write, read) 20 | value: ${{ steps.check.outputs.permission }} 21 | 22 | runs: 23 | using: "composite" 24 | steps: 25 | - name: Check user permission 26 | id: check 27 | shell: bash 28 | env: 29 | GITHUB_TOKEN: ${{ inputs.token }} 30 | OWNER: ${{ github.repository_owner }} 31 | REPO: ${{ github.event.repository.name }} 32 | USERNAME: ${{ github.triggering_actor }} 33 | PERMISSION: ${{ inputs.require }} 34 | run: | 35 | # Fetch the collaborator permission level using the GitHub API 36 | response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ 37 | "https://api.github.com/repos/$OWNER/$REPO/collaborators/$USERNAME/permission") 38 | 39 | # Extract the permission level from the JSON response 40 | user_permission=$(echo $response | jq -r '.permission') 41 | echo "permission=${user_permission}" >> $GITHUB_OUTPUT 42 | 43 | # Compare the permission level with the required permission 44 | if [[ "$user_permission" == "$PERMISSION" || ( "$user_permission" == "admin" && "$PERMISSION" == "write" ) ]]; then 45 | echo "User has the required permission." 46 | echo "granted=true" >> $GITHUB_OUTPUT 47 | else 48 | echo "User does not have the required permission." 49 | echo "granted=false" >> $GITHUB_OUTPUT 50 | fi 51 | -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/HCaptchaCompat.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import android.content.Context; 4 | import android.content.pm.ApplicationInfo; 5 | import android.content.pm.PackageManager; 6 | import android.os.Build; 7 | import android.os.Bundle; 8 | import android.os.Parcelable; 9 | import androidx.annotation.NonNull; 10 | import androidx.annotation.Nullable; 11 | 12 | import java.io.Serializable; 13 | 14 | /** 15 | * Internal class not pollute code with SDK_INT >= VERSION_CODES.X checks 16 | */ 17 | final class HCaptchaCompat { 18 | private HCaptchaCompat() { 19 | } 20 | 21 | @SuppressWarnings({ "unchecked", "deprecation" }) 22 | static T getSerializable(Bundle bundle, @Nullable String key, Class clazz) { 23 | if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 24 | return bundle.getSerializable(key, clazz); 25 | } else { 26 | return (T) bundle.getSerializable(key); 27 | } 28 | } 29 | 30 | @SuppressWarnings({ "unchecked", "deprecation" }) 31 | static T getParcelable(Bundle bundle, @Nullable String key, Class clazz) { 32 | if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 33 | return bundle.getParcelable(key, clazz); 34 | } else { 35 | return (T) bundle.getParcelable(key); 36 | } 37 | } 38 | 39 | @SuppressWarnings({ "deprecation" }) 40 | static ApplicationInfo getApplicationInfo(@NonNull Context context) 41 | throws PackageManager.NameNotFoundException { 42 | if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 43 | return context.getPackageManager().getApplicationInfo( 44 | context.getPackageName(), PackageManager.ApplicationInfoFlags.of( 45 | PackageManager.GET_META_DATA)); 46 | } else { 47 | return context.getPackageManager().getApplicationInfo( 48 | context.getPackageName(), PackageManager.GET_META_DATA); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /sdk/src/test/java/com/hcaptcha/sdk/tasks/TaskTest.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk.tasks; 2 | 3 | import com.hcaptcha.sdk.HCaptchaException; 4 | import org.junit.Assert; 5 | import org.junit.Test; 6 | 7 | import java.util.concurrent.CountDownLatch; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | /** 11 | * Test class for Task concurrent modification scenarios. 12 | * Tests that CopyOnWriteArrayList prevents ConcurrentModificationException 13 | * when listeners modify the list during callbacks. 14 | */ 15 | public class TaskTest { 16 | 17 | private static final int TIMEOUT_SECONDS = 5; 18 | 19 | /** 20 | * Test implementation of Task to test the concurrent modification fix 21 | */ 22 | private static class TestTask extends Task { 23 | public void triggerSuccess(String result) { 24 | setResult(result); 25 | } 26 | 27 | public void triggerFailure(HCaptchaException exception) { 28 | setException(exception); 29 | } 30 | 31 | public void triggerOpen() { 32 | captchaOpened(); 33 | } 34 | } 35 | 36 | @Test 37 | public void testConcurrentModificationScenarios() throws Exception { 38 | final CountDownLatch latch = new CountDownLatch(2); 39 | final TestTask task = new TestTask(); 40 | 41 | final OnSuccessListener selfRemovingListener = new OnSuccessListener() { 42 | @Override 43 | public void onSuccess(String result) { 44 | task.removeOnSuccessListener(this); 45 | latch.countDown(); 46 | } 47 | }; 48 | 49 | final OnSuccessListener otherRemovingListener = new OnSuccessListener() { 50 | @Override 51 | public void onSuccess(String result) { 52 | task.removeOnSuccessListener(selfRemovingListener); 53 | latch.countDown(); 54 | } 55 | }; 56 | 57 | task.addOnSuccessListener(selfRemovingListener) 58 | .addOnSuccessListener(otherRemovingListener); 59 | 60 | task.triggerSuccess("test result"); 61 | 62 | Assert.assertTrue(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /compose-sdk/src/main/java/com/hcaptcha/sdk/HCaptchaComposeVerifier.kt: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk 2 | 3 | import android.app.Activity 4 | import androidx.annotation.VisibleForTesting 5 | import androidx.compose.runtime.State 6 | 7 | @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) 8 | internal class HCaptchaComposeVerifier( 9 | private val config: HCaptchaConfig, 10 | private val onResult: (HCaptchaResponse) -> Unit, 11 | private val helperState: State 12 | ) : IHCaptchaVerifier { 13 | 14 | private var verifyParams: HCaptchaVerifyParams? = null 15 | 16 | override fun onLoaded() { 17 | onResult(HCaptchaResponse.Event(HCaptchaEvent.Loaded)) 18 | 19 | if (config.hideDialog || config.size == HCaptchaSize.INVISIBLE) { 20 | helperState.value?.let { helper -> 21 | if (verifyParams != null) { 22 | helper.resetAndExecute(verifyParams) 23 | } else { 24 | helper.execute() 25 | } 26 | } ?: run { 27 | HCaptchaLog.w("HCaptchaWebViewHelper wasn't created, report bug to developer") 28 | onResult(HCaptchaResponse.Failure(HCaptchaError.INTERNAL_ERROR)) 29 | } 30 | } 31 | } 32 | 33 | override fun onOpen() { 34 | onResult(HCaptchaResponse.Event(HCaptchaEvent.Opened)) 35 | } 36 | 37 | override fun onSuccess(result: String) { 38 | onResult(HCaptchaResponse.Success(result)) 39 | } 40 | 41 | override fun onFailure(exception: HCaptchaException) { 42 | helperState.value?.takeIf { it.shouldRetry(exception) } 43 | ?.resetAndExecute(verifyParams) 44 | ?: onResult(HCaptchaResponse.Failure(exception.hCaptchaError)) 45 | } 46 | 47 | override fun startVerification(activity: Activity, verifyParams: HCaptchaVerifyParams?) { 48 | this.verifyParams = verifyParams 49 | error("startVerification should never be reached") 50 | } 51 | 52 | override fun reset() { 53 | error("reset should never be reached") 54 | } 55 | 56 | override fun destroy() { 57 | helperState.value?.destroy() ?: error("destroy should never be reached") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /benchmark/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'androidx.benchmark' 4 | } 5 | 6 | android { 7 | compileSdk 34 8 | namespace 'com.hcaptcha.sdk.bench' 9 | 10 | compileOptions { 11 | sourceCompatibility = JavaVersion.VERSION_1_8 12 | targetCompatibility = JavaVersion.VERSION_1_8 13 | } 14 | 15 | defaultConfig { 16 | minSdkVersion 16 17 | targetSdkVersion 34 18 | 19 | testInstrumentationRunner 'androidx.benchmark.junit4.AndroidBenchmarkRunner' 20 | testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "DEBUGGABLE,EMULATOR,LOW-BATTERY,UNLOCKED" 21 | } 22 | 23 | testBuildType = "release" 24 | 25 | buildTypes { 26 | debug { 27 | // Since debuggable can't be modified by gradle for library modules, 28 | // it must be done in a manifest - see src/androidTest/AndroidManifest.xml 29 | minifyEnabled true 30 | proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro" 31 | } 32 | release { 33 | isDefault = true 34 | } 35 | } 36 | } 37 | 38 | dependencies { 39 | androidTestImplementation 'androidx.test:runner:1.5.2' 40 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 41 | androidTestImplementation 'junit:junit:4.13.2' 42 | androidTestImplementation 'androidx.benchmark:benchmark-junit4:1.2.0' 43 | 44 | implementation project(path: ':sdk') 45 | implementation 'androidx.appcompat:appcompat:1.6.1' 46 | } 47 | 48 | // Workaround for: java.lang.ClassNotFoundException: com.android.tools.lint.client.api.Vendor 49 | lint.enabled = false 50 | 51 | tasks.register('pullBenchmarkData', Exec) { 52 | def packageName = android.namespace + ".test" 53 | 54 | commandLine java.nio.file.Paths.get(android.sdkDirectory.absolutePath, "platform-tools", "adb"), 55 | "pull", 56 | "/storage/emulated/0/Android/media/" \ 57 | + "${packageName}/additional_test_output/" \ 58 | + "${packageName.replace('.', '_')}-benchmarkData.json", 59 | "${projectDir}/build/outputs/" 60 | 61 | doFirst { 62 | println "pullBenchmarkData: ${commandLine.join(' ')}" 63 | } 64 | } -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/HCaptchaJSInterface.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import android.os.Handler; 4 | import android.util.Log; 5 | import android.webkit.JavascriptInterface; 6 | import androidx.annotation.Nullable; 7 | 8 | import com.fasterxml.jackson.core.JsonProcessingException; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | import lombok.NonNull; 11 | 12 | import java.io.Serializable; 13 | 14 | 15 | /** 16 | * The JavaScript Interface which bridges the js and the java code 17 | */ 18 | class HCaptchaJSInterface implements Serializable { 19 | public static final String JS_INTERFACE_TAG = "JSInterface"; 20 | 21 | @NonNull 22 | private final transient Handler handler; 23 | 24 | @Nullable 25 | private final String config; 26 | 27 | @NonNull 28 | private final transient IHCaptchaVerifier captchaVerifier; 29 | 30 | HCaptchaJSInterface(@NonNull final Handler handler, 31 | @NonNull final HCaptchaConfig config, 32 | @NonNull final IHCaptchaVerifier captchaVerifier) { 33 | this.handler = handler; 34 | this.captchaVerifier = captchaVerifier; 35 | String configJson = null; 36 | try { 37 | final ObjectMapper objectMapper = new ObjectMapper(); 38 | configJson = objectMapper.writeValueAsString(config); 39 | } catch (JsonProcessingException e) { 40 | Log.w(JS_INTERFACE_TAG, "Cannot prepare config for passing to WebView." 41 | + " A fallback config will be used"); 42 | } 43 | this.config = configJson; 44 | } 45 | 46 | @Nullable 47 | @JavascriptInterface 48 | public String getConfig() { 49 | return this.config; 50 | } 51 | 52 | @JavascriptInterface 53 | public void onPass(final String token) { 54 | HCaptchaLog.d("JSInterface.onPass"); 55 | handler.post(() -> captchaVerifier.onSuccess(token)); 56 | } 57 | 58 | @JavascriptInterface 59 | public void onError(final int errCode) { 60 | HCaptchaLog.d("JSInterface.onError %d", errCode); 61 | final HCaptchaError error = HCaptchaError.fromId(errCode); 62 | handler.post(() -> captchaVerifier.onFailure(new HCaptchaException(error))); 63 | } 64 | 65 | @JavascriptInterface 66 | public void onLoaded() { 67 | HCaptchaLog.d("JSInterface.onLoaded"); 68 | handler.post(captchaVerifier::onLoaded); 69 | } 70 | 71 | @JavascriptInterface 72 | public void onOpen() { 73 | HCaptchaLog.d("JSInterface.onOpen"); 74 | handler.post(captchaVerifier::onOpen); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /benchmark/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import static org.junit.Assert.fail; 4 | 5 | import android.os.Handler; 6 | import android.os.Looper; 7 | 8 | import androidx.benchmark.BenchmarkState; 9 | import androidx.benchmark.junit4.BenchmarkRule; 10 | import androidx.test.ext.junit.rules.ActivityScenarioRule; 11 | import androidx.test.ext.junit.runners.AndroidJUnit4; 12 | 13 | import org.junit.Ignore; 14 | import org.junit.Rule; 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | 18 | import java.util.concurrent.CountDownLatch; 19 | 20 | @Ignore("https://github.com/hCaptcha/hcaptcha-android-sdk/issues/101") 21 | @RunWith(AndroidJUnit4.class) 22 | public class HCaptchaWebViewHelperTest { 23 | @Rule 24 | public ActivityScenarioRule rule = new ActivityScenarioRule<>(TestActivity.class); 25 | 26 | @Rule 27 | public BenchmarkRule benchmarkRule = new BenchmarkRule(); 28 | 29 | public HCaptchaConfig config = HCaptchaConfig.builder() 30 | .siteKey("10000000-ffff-ffff-ffff-000000000001") 31 | .hideDialog(true) 32 | .loading(false) 33 | .build(); 34 | 35 | final HCaptchaInternalConfig internalConfig = HCaptchaInternalConfig.builder() 36 | .htmlProvider(new TestHCaptchaHtml()) 37 | .build(); 38 | 39 | @Test 40 | public void benchmarkWebViewLoad() { 41 | Handler handler = new Handler(Looper.getMainLooper()); 42 | final BenchmarkState state = benchmarkRule.getState(); 43 | while (state.keepRunning()) { 44 | state.pauseTiming(); 45 | final CountDownLatch latch = new CountDownLatch(1); 46 | try { 47 | rule.getScenario().onActivity(activity -> { 48 | HCaptchaWebView webView = new HCaptchaWebView(activity); 49 | state.resumeTiming(); 50 | new HCaptchaWebViewHelper( 51 | handler, 52 | activity, 53 | config, 54 | internalConfig, 55 | new TestHCaptchaVerifier() { 56 | @Override 57 | public void onLoaded() { 58 | latch.countDown(); 59 | } 60 | }, 61 | webView 62 | ); 63 | }); 64 | latch.await(); 65 | } catch (Exception e) { 66 | fail("benchmarkInvisibleVerificationColdRun failed"); 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/actions/android-benchmark-diff/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // - uses: actions/github-script@v6 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | const validateInput = (inputs) => { 8 | const usage = "Usage: script [reference-json] [updated-json] [--markdown|--json]" 9 | Object.entries(inputs).forEach(([inputName, filePath]) => { 10 | if (!filePath) { 11 | console.error(usage) 12 | console.error(`Error: missing input argument ${inputName}`) 13 | process.exit(1) 14 | } 15 | if (!fs.existsSync(filePath)) { 16 | console.error(usage) 17 | console.error(`Error: file ${filePath} not exists`) 18 | process.exit(2) 19 | } 20 | // TODO invalid JSON check 21 | }) 22 | } 23 | 24 | const benchmarkMapper = (e) => [`${e.className}.${e.name}`, e] 25 | 26 | const readBenchmarksFromFile = (filePath) => { 27 | let content = JSON.parse(fs.readFileSync(filePath)) 28 | return new Map(content.benchmarks.map(benchmarkMapper)) 29 | } 30 | 31 | const diffNumber = (n) => { 32 | const sign = Math.sign(n) === 1 ? '+' : '' 33 | return sign + (Number.isInteger(n) ? n : Number(n).toFixed(2)) 34 | } 35 | 36 | const report = (referencePath, updatedPath, isMarkdownOutput) => { 37 | validateInput({ referencePath, updatedPath }) 38 | 39 | const reference = readBenchmarksFromFile(referencePath) 40 | const updated = readBenchmarksFromFile(updatedPath) 41 | 42 | const diff = Array.from(updated, ([k, v]) => { 43 | let ref = reference.get(k) 44 | let upd = v 45 | 46 | if (ref === undefined) { 47 | ref = { 48 | metrics: { 49 | timeNs: { 50 | median: 0 51 | }, 52 | allocationCount: { 53 | median: 0 54 | } 55 | } 56 | } 57 | k += " (New)" 58 | } 59 | 60 | return [ 61 | k, 62 | diffNumber((upd.metrics.timeNs.median - ref.metrics.timeNs.median) / (10 ** 6)), 63 | diffNumber(upd.metrics.allocationCount.median - ref.metrics.allocationCount.median) 64 | ] 65 | }) 66 | 67 | if (!isMarkdownOutput) { 68 | return JSON.stringify(diff, null, 2) 69 | } 70 | 71 | const header = [ 72 | ["Test name", "Time ms. (median)", "Allocations (median)"], 73 | ["---------", "-----------------", "--------------------"] 74 | ] 75 | const markdown = header.concat(diff).map(e => e.join(' | ')).map(e => `| ${e} |`).join('\n'); 76 | return markdown 77 | } 78 | 79 | module.exports = { report }; 80 | 81 | if (require.main === module) { 82 | const [referencePath, updatedPath, outputType] = process.argv.slice(2) 83 | const isMarkdownOutput = outputType === "--markdown" 84 | const result = report(referencePath, updatedPath, isMarkdownOutput) 85 | console.log(result) 86 | } -------------------------------------------------------------------------------- /.github/actions/android-emulator-run/action.yml: -------------------------------------------------------------------------------- 1 | name: android-emulator-run 2 | description: Do run script after emulator boot (use cached AVD or create a new one) 3 | 4 | inputs: 5 | script: 6 | description: Script to run after emulator booted 7 | required: true 8 | arch: 9 | description: Emulator arch, supported values depends on runner 10 | required: true 11 | default: x86_64 12 | target: 13 | description: Emulator target. Supported `default` or `google_apis` values 14 | required: true 15 | default: default 16 | profile: 17 | description: Emulator profile 18 | required: true 19 | default: Nexus 6 20 | api-level: 21 | description: Emulator API level 22 | required: true 23 | default: '28' 24 | boot-options: 25 | description: Emulator boot options 26 | required: true 27 | default: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none 28 | fresh-avd: 29 | description: Force AVD creation and skip cache 30 | required: false 31 | default: 'false' 32 | 33 | runs: 34 | using: "composite" 35 | steps: 36 | - name: Cache AVD 37 | if: inputs.fresh-avd != 'true' 38 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 39 | id: avd-cache 40 | with: 41 | path: | 42 | ~/.android/avd/* 43 | ~/.android/adb* 44 | key: avd-api-${{ runner.os }}-${{ inputs.api-level }}-target-${{ inputs.target }} 45 | - if: runner.os == 'Linux' 46 | name: Enable KVM group perms 47 | shell: bash 48 | run: | 49 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 50 | sudo udevadm control --reload-rules 51 | sudo udevadm trigger --name-match=kvm 52 | - name: 'Create AVD' 53 | if: inputs.fresh-avd != 'true' && steps.avd-cache.outputs.cache-hit != 'true' 54 | uses: hCaptcha/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # v2 55 | with: 56 | arch: ${{ inputs.arch }} 57 | target: ${{ inputs.target }} 58 | profile: ${{ inputs.profile }} 59 | api-level: ${{ inputs.api-level }} 60 | emulator-options: ${{ inputs.boot-options }} 61 | force-avd-creation: false 62 | disable-animations: false 63 | script: echo "Generated AVD snapshot for caching." 64 | - uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # v2 65 | with: 66 | arch: ${{ inputs.arch }} 67 | target: ${{ inputs.target }} 68 | profile: ${{ inputs.profile }} 69 | api-level: ${{ inputs.api-level }} 70 | emulator-options: ${{ inputs.boot-options }} 71 | force-avd-creation: ${{ inputs.fresh-avd == 'true' }} 72 | disable-animations: true 73 | script: ${{ inputs.script }} 74 | -------------------------------------------------------------------------------- /test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaComposeVerifierTest.kt: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import androidx.compose.runtime.MutableState 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.ui.test.junit4.createAndroidComposeRule 8 | import androidx.test.annotation.UiThreadTest 9 | import androidx.test.ext.junit.runners.AndroidJUnit4 10 | import com.hcaptcha.sdk.test.TestActivity 11 | import org.junit.Rule 12 | import org.junit.Test 13 | import org.junit.runner.RunWith 14 | 15 | @RunWith(AndroidJUnit4::class) 16 | class HCaptchaComposeVerifierTest { 17 | @get:Rule 18 | val composeTestRule = createAndroidComposeRule() 19 | 20 | private val config = HCaptchaConfig.builder() 21 | .siteKey("siteKey") 22 | .retryPredicate { _, error -> error.hCaptchaError == HCaptchaError.NETWORK_ERROR } 23 | .build() 24 | 25 | private fun createVerifier(onResult: (HCaptchaResponse) -> Unit): IHCaptchaVerifier { 26 | val context = composeTestRule.activity 27 | val handler = Handler(Looper.getMainLooper()) 28 | val internalConfig = HCaptchaInternalConfig(com.hcaptcha.sdk.HCaptchaHtml()) 29 | val helper = mutableStateOf(null) 30 | 31 | val clazz = Class.forName("com.hcaptcha.sdk.HCaptchaComposeVerifier") 32 | val constructor = clazz.constructors.first() 33 | constructor.isAccessible = true 34 | 35 | val result = constructor.newInstance(config, onResult, helper) as IHCaptchaVerifier 36 | helper.value = HCaptchaWebViewHelper(handler, context, config, internalConfig, result, HCaptchaWebView(context)) 37 | return result 38 | } 39 | 40 | @Test 41 | @UiThreadTest 42 | fun retryHappens() { 43 | var onFailureCalled = false 44 | 45 | val subject = createVerifier { result -> 46 | when (result) { 47 | is HCaptchaResponse.Failure -> { 48 | onFailureCalled = true 49 | } 50 | 51 | is HCaptchaResponse.Event -> {} 52 | is HCaptchaResponse.Success -> error("unreachable") 53 | } 54 | } 55 | 56 | subject.onFailure(HCaptchaException(HCaptchaError.NETWORK_ERROR)) 57 | 58 | assert(!onFailureCalled) 59 | } 60 | 61 | @Test 62 | @UiThreadTest 63 | fun noRetryHappens() { 64 | var onFailureCalled = false 65 | 66 | val subject = createVerifier { result -> 67 | when (result) { 68 | is HCaptchaResponse.Failure -> { 69 | onFailureCalled = true 70 | } 71 | 72 | is HCaptchaResponse.Event -> {} 73 | is HCaptchaResponse.Success -> error("unreachable") 74 | } 75 | } 76 | 77 | subject.onFailure(HCaptchaException(HCaptchaError.CHALLENGE_CLOSED)) 78 | 79 | assert(onFailureCalled) 80 | } 81 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /compose-sdk/src/main/java/com/hcaptcha/sdk/HCaptchaCompose.kt: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk 2 | 3 | import android.content.Context 4 | import android.os.Handler 5 | import android.os.Looper 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.compose.foundation.clickable 9 | import androidx.compose.foundation.interaction.MutableInteractionSource 10 | import androidx.compose.foundation.layout.Arrangement 11 | import androidx.compose.foundation.layout.Column 12 | import androidx.compose.foundation.layout.fillMaxSize 13 | import androidx.compose.foundation.layout.size 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.getValue 16 | import androidx.compose.runtime.mutableStateOf 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.runtime.setValue 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.platform.LocalContext 22 | import androidx.compose.ui.platform.testTag 23 | import androidx.compose.ui.unit.dp 24 | import androidx.compose.ui.viewinterop.AndroidView 25 | import androidx.compose.ui.window.Dialog 26 | 27 | @Composable 28 | public fun HCaptchaCompose(config: HCaptchaConfig, onResult: (HCaptchaResponse) -> Unit) { 29 | HCaptchaLog.sDiagnosticsLogEnabled = config.diagnosticLog 30 | 31 | val context = LocalContext.current 32 | val handler = Handler(Looper.getMainLooper()) 33 | val internalConfig = HCaptchaInternalConfig(com.hcaptcha.sdk.HCaptchaHtml()) 34 | 35 | val helper = remember { mutableStateOf(null) } 36 | val verifier = remember { HCaptchaComposeVerifier(config, onResult, helper) } 37 | val preloadedWebView = remember { 38 | HCaptchaWebView(context).apply { 39 | helper.value = HCaptchaWebViewHelper( 40 | handler, context, config, internalConfig, verifier, this 41 | ) 42 | } 43 | } 44 | var dismissed by remember { mutableStateOf(false) } 45 | 46 | val onDismissRequest: () -> Unit = { 47 | dismissed = true 48 | verifier.onFailure(HCaptchaException(HCaptchaError.CHALLENGE_CLOSED)); 49 | helper.value?.destroy() 50 | } 51 | 52 | val webViewFactory: (Context) -> View = { 53 | preloadedWebView.apply { 54 | (parent as? ViewGroup)?.removeView(this) 55 | } 56 | } 57 | 58 | HCaptchaLog.d("HCaptchaCompose($config)") 59 | 60 | if (config.hideDialog) { 61 | AndroidView( 62 | modifier = Modifier.size(0.dp), 63 | factory = webViewFactory 64 | ) 65 | } else if (!dismissed) { 66 | Dialog( 67 | onDismissRequest = onDismissRequest 68 | ) { 69 | Column( 70 | modifier = Modifier 71 | .testTag("dialogRoot") 72 | .fillMaxSize() 73 | .clickable( 74 | interactionSource = MutableInteractionSource(), 75 | indication = null, 76 | onClick = onDismissRequest 77 | ), 78 | verticalArrangement = Arrangement.Center, 79 | horizontalAlignment = Alignment.CenterHorizontally 80 | ) { 81 | AndroidView(factory = webViewFactory) 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /benchmark/src/androidTest/java/com/hcaptcha/sdk/HCaptchaBenchmarkTest.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import static org.junit.Assert.fail; 4 | 5 | import androidx.benchmark.BenchmarkState; 6 | import androidx.benchmark.junit4.BenchmarkRule; 7 | import androidx.test.ext.junit.rules.ActivityScenarioRule; 8 | import androidx.test.ext.junit.runners.AndroidJUnit4; 9 | 10 | import org.junit.Rule; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | 14 | import java.util.concurrent.CountDownLatch; 15 | 16 | @RunWith(AndroidJUnit4.class) 17 | public class HCaptchaBenchmarkTest { 18 | 19 | @Rule 20 | public ActivityScenarioRule rule = new ActivityScenarioRule<>(TestActivity.class); 21 | 22 | @Rule 23 | public BenchmarkRule benchmarkRule = new BenchmarkRule(); 24 | 25 | public HCaptchaConfig config = HCaptchaConfig.builder() 26 | .siteKey("10000000-ffff-ffff-ffff-000000000001") 27 | .hideDialog(true) 28 | .loading(false) 29 | .build(); 30 | 31 | final HCaptchaInternalConfig internalConfig = HCaptchaInternalConfig.builder() 32 | .htmlProvider(new TestHCaptchaHtml()) 33 | .build(); 34 | 35 | @Test 36 | public void benchmarkInvisibleSetup() { 37 | rule.getScenario().onActivity(activity -> { 38 | final BenchmarkState state = benchmarkRule.getState(); 39 | while (state.keepRunning()) { 40 | HCaptcha.getClient(activity, internalConfig).setup(config); 41 | } 42 | }); 43 | } 44 | 45 | @Test 46 | public void benchmarkInvisibleVerification() { 47 | final BenchmarkState state = benchmarkRule.getState(); 48 | while (state.keepRunning()) { 49 | state.pauseTiming(); 50 | final CountDownLatch latch = new CountDownLatch(1); 51 | try { 52 | rule.getScenario().onActivity(activity -> { 53 | HCaptcha hCaptcha = HCaptcha.getClient(activity, internalConfig).setup(config); 54 | state.resumeTiming(); 55 | hCaptcha.verifyWithHCaptcha() 56 | .addOnSuccessListener(response -> latch.countDown()) 57 | .addOnFailureListener(exception -> latch.countDown()); 58 | 59 | }); 60 | latch.await(); 61 | } catch (InterruptedException e) { 62 | fail("benchmarkInvisibleVerification failed"); 63 | } 64 | } 65 | } 66 | 67 | @Test 68 | public void benchmarkInvisibleVerificationColdRun() { 69 | final BenchmarkState state = benchmarkRule.getState(); 70 | while (state.keepRunning()) { 71 | state.pauseTiming(); 72 | final CountDownLatch latch = new CountDownLatch(1); 73 | try { 74 | rule.getScenario().onActivity(activity -> { 75 | state.resumeTiming(); 76 | HCaptcha.getClient(activity, internalConfig).verifyWithHCaptcha(config) 77 | .addOnSuccessListener(response -> latch.countDown()); 78 | 79 | }); 80 | latch.await(); 81 | } catch (InterruptedException e) { 82 | fail("benchmarkInvisibleVerificationColdRun failed"); 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /compose-sdk/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'maven-publish' 3 | id 'com.android.library' 4 | id 'org.jetbrains.kotlin.android' 5 | id "pmd" 6 | id "jacoco" 7 | id "checkstyle" 8 | id "com.github.spotbugs" version "5.2.3" 9 | id "org.owasp.dependencycheck" version "7.1.1" 10 | id "org.sonarqube" version "3.4.0.2513" 11 | } 12 | 13 | android { 14 | namespace 'com.hcaptcha.compose' 15 | compileSdk 35 16 | 17 | defaultConfig { 18 | minSdk 21 19 | 20 | // See https://developer.android.com/studio/publish/versioning 21 | // versionCode must be integer and be incremented by one for every new update 22 | // android system uses this to prevent downgrades 23 | versionCode 56 24 | 25 | // version number visible to the user 26 | // should follow semantic versioning (See https://semver.org) 27 | versionName "4.4.0" 28 | 29 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 30 | consumerProguardFiles "consumer-rules.pro" 31 | } 32 | 33 | buildTypes { 34 | release { 35 | minifyEnabled false 36 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 37 | } 38 | } 39 | compileOptions { 40 | sourceCompatibility JavaVersion.VERSION_1_8 41 | targetCompatibility JavaVersion.VERSION_1_8 42 | } 43 | 44 | kotlinOptions { 45 | jvmTarget = JavaVersion.VERSION_1_8 46 | } 47 | 48 | buildFeatures { // Enables Jetpack Compose for this module 49 | compose = true 50 | } 51 | 52 | composeOptions { 53 | kotlinCompilerExtensionVersion = "$compose_version" 54 | } 55 | 56 | publishing { 57 | singleVariant('release') { 58 | withSourcesJar() 59 | withJavadocJar() 60 | } 61 | } 62 | } 63 | 64 | dependencies { 65 | api project(':sdk') 66 | implementation "androidx.compose.foundation:foundation:$compose_version" 67 | } 68 | 69 | project.afterEvaluate { 70 | publishing { 71 | repositories { 72 | } 73 | 74 | publications { 75 | release(MavenPublication) { 76 | from components.release 77 | 78 | groupId = 'com.hcaptcha' 79 | artifactId = 'compose-sdk' 80 | version = android.defaultConfig.versionName 81 | 82 | pom { 83 | name = 'Android Jetpack Compose SDK hCaptcha' 84 | description = 'This SDK provides a wrapper for hCaptcha and ready to use Jetpack Compose Component' 85 | url = 'https://github.com/hCaptcha/hcaptcha-jetpack-compose' 86 | licenses { 87 | license { 88 | name = 'MIT License' 89 | url = 'https://github.com/hCaptcha/hcaptcha-jetpack-compose-sdk/blob/main/LICENSE' 90 | } 91 | } 92 | scm { 93 | connection = 'scm:git:git://github.com/hCaptcha/hcaptcha-android-sdk.git' 94 | developerConnection = 'scm:git:ssh://github.com:hCaptcha/hcaptcha-android-sdk.git' 95 | url = 'https://github.com/hCaptcha/hcaptcha-android-sdk' 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | 103 | apply from: "$rootProject.projectDir/gradle/shared/code-quality.gradle" 104 | -------------------------------------------------------------------------------- /sdk/src/test/java/com/hcaptcha/sdk/HCaptchaVerifyParamsTest.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertNotNull; 5 | import static org.junit.Assert.assertNull; 6 | 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import org.json.JSONObject; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.junit.runners.JUnit4; 12 | 13 | @RunWith(JUnit4.class) 14 | public class HCaptchaVerifyParamsTest { 15 | 16 | private static final String TEST_PHONE_PREFIX = "44"; 17 | private static final String TEST_PHONE_NUMBER = "+44123456789"; 18 | private static final String TEST_RQDATA = "test-rqdata-string"; 19 | 20 | @Test 21 | public void test_verify_params_with_both_values() { 22 | final HCaptchaVerifyParams params = HCaptchaVerifyParams.builder() 23 | .phonePrefix(TEST_PHONE_PREFIX) 24 | .phoneNumber(TEST_PHONE_NUMBER) 25 | .build(); 26 | 27 | assertNotNull(params); 28 | assertEquals(TEST_PHONE_PREFIX, params.getPhonePrefix()); 29 | assertEquals(TEST_PHONE_NUMBER, params.getPhoneNumber()); 30 | } 31 | 32 | @Test 33 | public void test_verify_params_with_rqdata() { 34 | final HCaptchaVerifyParams params = HCaptchaVerifyParams.builder() 35 | .rqdata(TEST_RQDATA) 36 | .build(); 37 | 38 | assertNotNull(params); 39 | assertNull(params.getPhonePrefix()); 40 | assertNull(params.getPhoneNumber()); 41 | assertEquals(TEST_RQDATA, params.getRqdata()); 42 | } 43 | 44 | @Test 45 | public void test_verify_params_with_all_values() { 46 | final HCaptchaVerifyParams params = HCaptchaVerifyParams.builder() 47 | .phonePrefix(TEST_PHONE_PREFIX) 48 | .phoneNumber(TEST_PHONE_NUMBER) 49 | .rqdata(TEST_RQDATA) 50 | .build(); 51 | 52 | assertNotNull(params); 53 | assertEquals(TEST_PHONE_PREFIX, params.getPhonePrefix()); 54 | assertEquals(TEST_PHONE_NUMBER, params.getPhoneNumber()); 55 | assertEquals(TEST_RQDATA, params.getRqdata()); 56 | } 57 | 58 | @Test 59 | public void test_verify_params_empty() { 60 | final HCaptchaVerifyParams params = HCaptchaVerifyParams.builder().build(); 61 | 62 | assertNotNull(params); 63 | assertNull(params.getPhonePrefix()); 64 | assertNull(params.getPhoneNumber()); 65 | } 66 | 67 | @Test 68 | public void test_json_serialization_annotations_not_swapped() throws Exception { 69 | final HCaptchaVerifyParams params = HCaptchaVerifyParams.builder() 70 | .phonePrefix(TEST_PHONE_PREFIX) 71 | .phoneNumber(TEST_PHONE_NUMBER) 72 | .rqdata(TEST_RQDATA) 73 | .build(); 74 | 75 | final ObjectMapper mapper = new ObjectMapper(); 76 | final String json = mapper.writeValueAsString(params); 77 | 78 | final JSONObject jsonObject = new JSONObject(json); 79 | 80 | // Verify that the JSON field names match the @JsonProperty annotations 81 | assertNotNull(jsonObject); 82 | assertEquals(TEST_PHONE_PREFIX, jsonObject.getString("mfa_phoneprefix")); 83 | assertEquals(TEST_PHONE_NUMBER, jsonObject.getString("mfa_phone")); 84 | assertEquals(TEST_RQDATA, jsonObject.getString("rqdata")); 85 | 86 | // Verify that the original field names are NOT present in JSON 87 | assertEquals(false, jsonObject.has("phonePrefix")); 88 | assertEquals(false, jsonObject.has("phoneNumber")); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/HCaptchaError.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import lombok.Getter; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * Enum with all possible hCaptcha errors. 11 | * It contains both errors related to the android sdk instance and js client errors. 12 | * More info about js client errors here: https://docs.hcaptcha.com/configuration#error-codes 13 | */ 14 | @Getter 15 | public enum HCaptchaError implements Serializable { 16 | 17 | /** 18 | * Internet connection is missing. 19 | * 20 | * Make sure AndroidManifest requires internet permission: 21 | * {@code {@literal <}uses-permission android:name="android.permission.INTERNET" /{@literal >}} 22 | */ 23 | NETWORK_ERROR(7, "No internet connection"), 24 | 25 | /** 26 | * Invalid data is not accepted by endpoints. 27 | */ 28 | INVALID_DATA(8, "Invalid data is not accepted by endpoints"), 29 | 30 | /** 31 | * User may need to select the checkbox or if invisible programmatically call execute to 32 | * initiate the challenge again. 33 | */ 34 | CHALLENGE_ERROR(9, "Challenge encountered error on setup"), 35 | 36 | /** 37 | * User may need to select the checkbox or if invisible programmatically call execute to 38 | * initiate the challenge again. 39 | */ 40 | INTERNAL_ERROR(10, "hCaptcha client encountered an internal error"), 41 | 42 | /** 43 | * hCaptcha challenge expired 44 | */ 45 | SESSION_TIMEOUT(15, "Session Timeout"), 46 | 47 | /** 48 | * hCaptcha token expired 49 | */ 50 | TOKEN_TIMEOUT(16, "Token Timeout"), 51 | 52 | /** 53 | * User closed the challenge by pressing `back` button or touching the outside of the dialog. 54 | */ 55 | CHALLENGE_CLOSED(30, "Challenge Closed"), 56 | 57 | /** 58 | * Rate limited due to too many tries. 59 | */ 60 | RATE_LIMITED(31, "Rate Limited"), 61 | 62 | /** 63 | * Invalid custom theme 64 | */ 65 | INVALID_CUSTOM_THEME(32, "Invalid custom theme"), 66 | 67 | /** 68 | * Insecure HTTP request intercepted 69 | */ 70 | INSECURE_HTTP_REQUEST_ERROR(33, "Insecure resource requested"), 71 | 72 | /** 73 | * Error handling verify parameters on JavaScript side 74 | */ 75 | VERIFY_PARAMS_ERROR(34, "Error handling verify parameters"), 76 | 77 | /** 78 | * Generic error for unknown situations - should never happen. 79 | */ 80 | ERROR(29, "Unknown error"); 81 | 82 | /** 83 | * The integer encoding of the enum 84 | */ 85 | private final int errorId; 86 | 87 | /** 88 | * The error message 89 | */ 90 | private final String message; 91 | 92 | HCaptchaError(final int errorId, final String message) { 93 | this.errorId = errorId; 94 | this.message = message; 95 | } 96 | 97 | @NonNull 98 | @Override 99 | public String toString() { 100 | return message; 101 | } 102 | 103 | /** 104 | * Finds the enum based on the integer encoding 105 | * 106 | * @param errorId the integer encoding 107 | * @return the {@link HCaptchaError} object 108 | * @throws RuntimeException when no match 109 | */ 110 | @NonNull 111 | public static HCaptchaError fromId(final int errorId) { 112 | final HCaptchaError[] errors = HCaptchaError.values(); 113 | for (final HCaptchaError error : errors) { 114 | if (error.errorId == errorId) { 115 | return error; 116 | } 117 | } 118 | throw new RuntimeException("Unsupported error id: " + errorId); 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Prerequisites software 2 | 3 | - Android Studio 4 | 5 | # Testing 6 | 7 | There is automated testing for every `push` command through github actions (see `.github/workflows/ci.yml`). 8 | 9 | You can manually test before pushing by running both unit tests and instrumented tests: 10 | * `./gradlew sdk:test` 11 | * `./gradlew test:connectedAndroidTest` 12 | * `./gradlew test:connectedAndroidTest -P testingMinimizedBuild=true -P android.enableR8.fullMode=false` 13 | 14 | ## JDK Versions 15 | 16 | Testing requires JDK 17+ for pre-commit hooks. Quickstart on Mac: 17 | 18 | ```bash 19 | brew install --cask temurin@17 20 | JAVA_HOME=$(/usr/libexec/java_home -v 17) PATH="$JAVA_HOME/bin:$PATH" git commit -m "my msg" 21 | ``` 22 | 23 | ## Manual testing 24 | 25 | + {normal,invisible,compact} -> verify -> success -> mark used 26 | + {normal,invisible,compact} -> verify -> success -> token timeout 27 | 28 | + {normal,invisible,compact} -> verify -> touch outside -> challenge closed 29 | + {normal,invisible,compact} -> verify -> back button -> challenge closed 30 | 31 | + {normal,invisible,compact} -> verify -> rotate device (recreate activity, i.e. without `android:configChanges` in `AndroidManifest.xml`) -> hcaptcha gone, no callbacks fired 32 | + {normal,invisible,compact} -> verify -> send app to background -> open app from history again -> hcaptcha is displayed 33 | 34 | + {hide dialog} -> verify -> token obtained -> mark used 35 | 36 | ## How to end-to-end test SDK integration before release 37 | 38 | ### When this make sense 39 | 40 | * Proguard configuration update 41 | * API Changes 42 | * Breaking Changes 43 | 44 | ### How to do this 45 | 46 | To install the SDK for a specific pull request (PR) or git branch in the same way as end-developers do, i.e. as a Gradle dependency, follow the steps below: 47 | 48 | 1. Update your dependency in `example-app/build.gradle` by replacing: 49 | ```groovy 50 | dependencies { 51 | // ... 52 | implementation project(path: ':sdk') 53 | // ... 54 | } 55 | ``` 56 | 57 | with: 58 | 59 | ```groovy 60 | dependencies { 61 | // ... 62 | implementation "com.github.hCaptcha.hcaptcha-android-sdk:sdk:BRANCH_NAME-SNAPSHOT" 63 | // or 64 | implementation "com.github.hCaptcha.hcaptcha-android-sdk:sdk:pull/PR_NUMBER/head-SNAPSHOT" 65 | } 66 | ``` 67 | 1. Build `example-app` for `release` variant 68 | 1. Test `example-app` 69 | 70 | > NOTE: JitPack builds dependencies on-demand, i.e. once Gradle requests the dependency. 71 | > If the dependency for the specified PR branch has not been built yet, Gradle may fail with a timeout error. 72 | > It can take 5-10 minutes for JitPack to build and make the dependency available to Gradle, so please be patient. 73 | 74 | # Publishing 75 | 76 | To publish a new version follow the next steps: 77 | 78 | 1. Bump versions in the [`sdk/build.gradle`](./sdk/build.gradle) file: 79 | * `android.defaultConfig.versionCode`: increment by **1** (next integer) 80 | * `android.defaultConfig.versionName`: [Semantic Versioning](https://semver.org) 81 | 2. Use the same `versionCode` and `versionName` for [`compose-sdk/build.gradle`](./compose-sdk/build.gradle) 82 | 3. Update [`CHANGES.md`](./CHANGES.md) with changes since last version 83 | 4. Create a [Github Release](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/managing-releases-in-a-repository#creating-a-release) with the **SAME** version from step 1 (**without** a prefix such as `v`) 84 | * JitPack's automatic process will be triggered upon first installation of the new package version 85 | 86 | # Known issues 87 | 88 | ### Android Studio Lombok plugin doesn't work (generated methods not found) 89 | 90 | Install lombok plugin from https://github.com/mplushnikov/lombok-intellij-plugin/issues/1130#issuecomment-1108316265 91 | -------------------------------------------------------------------------------- /sdk/src/main/java/com/hcaptcha/sdk/HCaptchaHeadlessWebView.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import android.app.Activity; 4 | import android.os.Handler; 5 | import android.os.Looper; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.webkit.WebView; 9 | import androidx.annotation.Nullable; 10 | 11 | import lombok.Getter; 12 | import lombok.NonNull; 13 | 14 | final class HCaptchaHeadlessWebView implements IHCaptchaVerifier { 15 | @Getter 16 | @NonNull 17 | private final HCaptchaConfig config; 18 | 19 | @NonNull 20 | private final HCaptchaStateListener listener; 21 | 22 | @NonNull 23 | private final HCaptchaWebViewHelper webViewHelper; 24 | 25 | private boolean webViewLoaded; 26 | private boolean shouldExecuteOnLoad; 27 | private boolean shouldResetOnLoad; 28 | 29 | @Nullable 30 | private HCaptchaVerifyParams verifyParams; 31 | 32 | HCaptchaHeadlessWebView(@NonNull final Activity activity, 33 | @NonNull final HCaptchaConfig config, 34 | @NonNull final HCaptchaInternalConfig internalConfig, 35 | @NonNull final HCaptchaStateListener listener) { 36 | HCaptchaLog.d("HeadlessWebView.init"); 37 | this.config = config; 38 | this.listener = listener; 39 | final HCaptchaWebView webView = new HCaptchaWebView(activity); 40 | webView.setId(R.id.webView); 41 | webView.setVisibility(View.GONE); 42 | if (webView.getParent() == null) { 43 | final ViewGroup rootView = (ViewGroup) activity.getWindow().getDecorView().getRootView(); 44 | rootView.addView(webView); 45 | } 46 | webViewHelper = new HCaptchaWebViewHelper( 47 | new Handler(Looper.getMainLooper()), activity, config, internalConfig, this, webView); 48 | } 49 | 50 | @Override 51 | public void startVerification(@NonNull Activity activity, @Nullable HCaptchaVerifyParams params) { 52 | this.verifyParams = params; 53 | if (webViewLoaded) { 54 | // Safe to execute 55 | webViewHelper.resetAndExecute(params); 56 | } else { 57 | shouldExecuteOnLoad = true; 58 | } 59 | } 60 | 61 | @Override 62 | public void onFailure(@NonNull final HCaptchaException exception) { 63 | final boolean silentRetry = webViewHelper.shouldRetry(exception); 64 | if (silentRetry) { 65 | webViewHelper.resetAndExecute(verifyParams); 66 | } else { 67 | listener.onFailure(exception); 68 | } 69 | } 70 | 71 | @Override 72 | public void onSuccess(final String token) { 73 | listener.onSuccess(token); 74 | } 75 | 76 | @Override 77 | public void onLoaded() { 78 | webViewLoaded = true; 79 | if (shouldResetOnLoad) { 80 | shouldResetOnLoad = false; 81 | reset(); 82 | } else if (shouldExecuteOnLoad) { 83 | shouldExecuteOnLoad = false; 84 | webViewHelper.resetAndExecute(verifyParams); 85 | } 86 | } 87 | 88 | @Override 89 | public void onOpen() { 90 | listener.onOpen(); 91 | } 92 | 93 | @Override 94 | public void reset() { 95 | if (webViewLoaded) { 96 | webViewHelper.reset(); 97 | final WebView webView = webViewHelper.getWebView(); 98 | if (webView.getParent() != null) { 99 | ((ViewGroup) webView.getParent()).removeView(webView); 100 | } 101 | } else { 102 | shouldResetOnLoad = true; 103 | } 104 | } 105 | 106 | @Override 107 | public void destroy() { 108 | webViewHelper.destroy(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaTestHtml.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | class HCaptchaTestHtml implements IHCaptchaHtmlProvider { 6 | 7 | private final boolean callBridgeOnLoaded; 8 | 9 | HCaptchaTestHtml() { 10 | this(true); 11 | } 12 | 13 | HCaptchaTestHtml(boolean callBridgeOnLoaded) { 14 | this.callBridgeOnLoaded = callBridgeOnLoaded; 15 | } 16 | 17 | @Override 18 | @NonNull 19 | public String getHtml() { 20 | return "\n" 21 | + "\n" 22 | + " \n" 23 | + " \n" 30 | + "\n" 31 | + "\n" 32 | + "
\n" 33 | + " \n" 34 | + " \n" 35 | + " \n" 36 | + " Send SMS\n" 37 | + " Open in Browser\n" 38 | + " \n" 73 | + "\n" 74 | + "\n"; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /sdk/src/test/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java: -------------------------------------------------------------------------------- 1 | package com.hcaptcha.sdk; 2 | 3 | import static org.mockito.ArgumentMatchers.any; 4 | import static org.mockito.ArgumentMatchers.anyString; 5 | import static org.mockito.Mockito.mock; 6 | import static org.mockito.Mockito.mockStatic; 7 | import static org.mockito.Mockito.times; 8 | import static org.mockito.Mockito.verify; 9 | import static org.mockito.Mockito.when; 10 | import static org.mockito.Mockito.withSettings; 11 | 12 | import android.content.Context; 13 | import android.os.Handler; 14 | import android.util.Log; 15 | import android.view.ViewGroup; 16 | import android.view.ViewParent; 17 | import android.webkit.WebSettings; 18 | 19 | import org.junit.After; 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | import org.junit.runner.RunWith; 23 | import org.mockito.Mock; 24 | import org.mockito.MockedStatic; 25 | import org.mockito.MockitoAnnotations; 26 | import org.mockito.junit.MockitoJUnitRunner; 27 | 28 | @RunWith(MockitoJUnitRunner.class) 29 | public class HCaptchaWebViewHelperTest { 30 | private static final String MOCK_HTML = ""; 31 | 32 | @Mock 33 | Context context; 34 | 35 | @Mock 36 | HCaptchaConfig config; 37 | 38 | @Mock 39 | HCaptchaInternalConfig internalConfig; 40 | 41 | @Mock 42 | IHCaptchaVerifier captchaVerifier; 43 | 44 | @Mock 45 | HCaptchaWebView webView; 46 | 47 | @Mock 48 | WebSettings webSettings; 49 | 50 | @Mock 51 | Handler handler; 52 | 53 | @Mock 54 | IHCaptchaHtmlProvider htmlProvider; 55 | 56 | MockedStatic 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 | --------------------------------------------------------------------------------