diff --git a/.github/actions/android-emulator-run/action.yml b/.github/actions/android-emulator-run/action.yml
new file mode 100644
index 00000000..09288a97
--- /dev/null
+++ b/.github/actions/android-emulator-run/action.yml
@@ -0,0 +1,68 @@
+name: android-emulator-run
+description: Do run script after emulator boot (use cached AVD or create a new one)
+
+inputs:
+ script:
+ description: Script to run after emulator booted
+ required: true
+ arch:
+ description: Emulator arch, supported values depends on runner
+ required: true
+ default: x86_64
+ target:
+ description: Emulator target. Supported `default` or `google_apis` values
+ required: true
+ default: default
+ profile:
+ description: Emulator profile
+ required: true
+ default: Nexus 6
+ api-level:
+ description: Emulator API level
+ required: true
+ default: '28'
+ boot-options:
+ description: Emulator boot options
+ required: true
+ default: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
+
+runs:
+ using: "composite"
+ steps:
+ - name: Cache AVD
+ uses: actions/cache@v4
+ id: avd-cache
+ with:
+ path: |
+ ~/.android/avd/*
+ ~/.android/adb*
+ key: avd-api-${{ runner.os }}-${{ inputs.api-level }}-target-${{ inputs.target }}
+ - if: runner.os == 'Linux'
+ name: Enable KVM group perms
+ shell: bash
+ run: |
+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+ sudo udevadm control --reload-rules
+ sudo udevadm trigger --name-match=kvm
+ - name: 'Create AVD'
+ if: steps.avd-cache.outputs.cache-hit != 'true'
+ uses: reactivecircus/android-emulator-runner@v2
+ with:
+ arch: ${{ inputs.arch }}
+ target: ${{ inputs.target }}
+ profile: ${{ inputs.profile }}
+ api-level: ${{ inputs.api-level }}
+ emulator-options: ${{ inputs.boot-options }}
+ force-avd-creation: false
+ disable-animations: false
+ script: echo "Generated AVD snapshot for caching."
+ - uses: reactivecircus/android-emulator-runner@v2
+ with:
+ arch: ${{ inputs.arch }}
+ target: ${{ inputs.target }}
+ profile: ${{ inputs.profile }}
+ api-level: ${{ inputs.api-level }}
+ emulator-options: ${{ inputs.boot-options }}
+ force-avd-creation: false
+ disable-animations: true
+ script: ${{ inputs.script }}
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b5618df8..d3f0f852 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,7 +26,7 @@ jobs:
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: adopt
- - uses: gradle/gradle-build-action@v3
+ - uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: false
- name: 'Build'
@@ -41,7 +41,7 @@ jobs:
build-matrix:
name: 'Build (target:${{ matrix.target }} compile:${{ matrix.compile }} appcompat: ${{ matrix.appcompat }})'
- needs: [ test ]
+ needs: test
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -50,16 +50,16 @@ jobs:
- compile: 34
target: 34
appcompat: 1.6.1
- - compile: 33
+ - compile: 34
target: 33
appcompat: 1.5.1
- - compile: 32
+ - compile: 34
target: 32
appcompat: 1.4.2
- - compile: 32
+ - compile: 34
target: 30
appcompat: 1.3.1
- - compile: 30
+ - compile: 34
target: 30
appcompat: 1.3.1
steps:
@@ -68,7 +68,7 @@ jobs:
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: adopt
- - uses: gradle/gradle-build-action@v3
+ - uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: false
- run: |
@@ -78,55 +78,22 @@ jobs:
test-ui:
name: 'Test UI'
- runs-on: macos-latest
+ needs: test
+ runs-on: ubuntu-latest
timeout-minutes: 20
- strategy:
- fail-fast: false
- matrix:
- api-level: [29]
- target: [default]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: adopt
- - uses: gradle/gradle-build-action@v3
+ - uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: false
- - name: 'Cache AVD'
- uses: actions/cache@v4
- id: avd-cache
- with:
- path: |
- ~/.android/avd/*
- ~/.android/adb*
- key: avd-api-${{ matrix.api-level }}-target-${{ matrix.target }}
- - name: 'Create AVD'
- if: steps.avd-cache.outputs.cache-hit != 'true'
- uses: reactivecircus/android-emulator-runner@v2.29.0
- with:
- api-level: ${{ matrix.api-level }}
- target: ${{ matrix.target }}
- emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
- force-avd-creation: false
- disable-animations: false
- arch: x86_64
- profile: Nexus 6
- script: echo "Generated AVD snapshot for caching."
- - name: 'Tests'
- uses: reactivecircus/android-emulator-runner@v2.29.0
+ - name: Run tests
+ uses: ./.github/actions/android-emulator-run
with:
- api-level: ${{ matrix.api-level }}
- target: ${{ matrix.target }}
- emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
- force-avd-creation: false
- disable-animations: true
- arch: x86_64
- profile: Nexus 6
- script: |
- brew install parallel
- parallel --retries 3 ::: "./gradlew test:connectedCheck"
+ script: "parallel --retries 3 ::: './gradlew test:connectedCheck'"
- if: failure()
uses: actions/upload-artifact@v4
with:
@@ -137,55 +104,22 @@ jobs:
test-minified:
name: 'Test UI Minified'
- runs-on: macos-latest
+ needs: [ test-ui ]
+ runs-on: ubuntu-latest
timeout-minutes: 20
- strategy:
- fail-fast: false
- matrix:
- api-level: [29]
- target: [default]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: adopt
- - uses: gradle/gradle-build-action@v3
+ - uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: false
- - name: 'Cache AVD'
- uses: actions/cache@v4
- id: avd-cache
- with:
- path: |
- ~/.android/avd/*
- ~/.android/adb*
- key: avd-api-${{ matrix.api-level }}-target-${{ matrix.target }}
- - name: 'Create AVD'
- if: steps.avd-cache.outputs.cache-hit != 'true'
- uses: reactivecircus/android-emulator-runner@v2.29.0
+ - name: Run tests
+ uses: ./.github/actions/android-emulator-run
with:
- api-level: ${{ matrix.api-level }}
- target: ${{ matrix.target }}
- emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
- force-avd-creation: false
- disable-animations: false
- arch: x86_64
- profile: Nexus 6
- script: echo "Generated AVD snapshot for caching."
- - name: 'Tests'
- uses: reactivecircus/android-emulator-runner@v2.29.0
- with:
- api-level: ${{ matrix.api-level }}
- target: ${{ matrix.target }}
- emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
- force-avd-creation: false
- disable-animations: true
- arch: x86_64
- profile: Nexus 6
- script: |
- brew install parallel
- parallel --retries 3 ::: "./gradlew test:connectedCheck -P testingMinimizedBuild=true -P android.enableR8.fullMode=false"
+ script: "parallel --retries 3 ::: './gradlew test:connectedCheck -P testingMinimizedBuild=true -P android.enableR8.fullMode=false'"
- if: failure()
uses: actions/upload-artifact@v4
with:
@@ -196,52 +130,34 @@ jobs:
test-benchmark:
name: 'Test Benchmark'
- runs-on: macos-latest
+ needs: test
+ # ubuntu-latest fails with JNI ERROR (app bug): weak global reference table overflow (max=51200)
+ # macos-latest i.e. macos-14 https://github.com/ReactiveCircus/android-emulator-runner/issues/324
+ runs-on: macos-13
timeout-minutes: 30
- strategy:
- fail-fast: false
- matrix:
- api-level: [29]
- target: [default]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: adopt
- - uses: gradle/gradle-build-action@v3
+ - uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: false
- - name: 'Cache AVD'
- uses: actions/cache@v4
- id: avd-cache
- with:
- path: |
- ~/.android/avd/*
- ~/.android/adb*
- key: avd-api-${{ matrix.api-level }}-target-${{ matrix.target }}
- - name: 'Create AVD'
- if: steps.avd-cache.outputs.cache-hit != 'true'
- uses: reactivecircus/android-emulator-runner@v2.29.0
+ - name: Run tests
+ uses: ./.github/actions/android-emulator-run
with:
- api-level: ${{ matrix.api-level }}
- target: ${{ matrix.target }}
- emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
- force-avd-creation: false
- disable-animations: false
- arch: x86_64
- profile: Nexus 6
- script: echo "Generated AVD snapshot for caching."
- - uses: reactivecircus/android-emulator-runner@v2.29.0
+ api-level: 29
+ script: |
+ adb uninstall com.hcaptcha.sdk.bench.test || true
+ ./gradlew benchmark:connectedReleaseAndroidTest
+ - if: failure()
+ uses: actions/upload-artifact@v4
with:
- api-level: ${{ matrix.api-level }}
- target: ${{ matrix.target }}
- emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
- force-avd-creation: false
- disable-animations: true
- arch: x86_64
- profile: Nexus 6
- script: ./gradlew benchmark:connectedReleaseAndroidTest
+ name: androidTest-benchmark-results
+ path: |
+ benchmark/build/outputs/androidTest-results
+ benchmark/build/reports/androidTests
- name: Diff benchmark result
id: diff-benchmark
uses: ./.github/actions/android-benchmark-diff
@@ -284,7 +200,7 @@ jobs:
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: adopt
- - uses: gradle/gradle-build-action@v3
+ - uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: false
- uses: actions/cache@v4
@@ -310,7 +226,7 @@ jobs:
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: adopt
- - uses: gradle/gradle-build-action@v3
+ - uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: false
- name: 'Build'
diff --git a/.gitignore b/.gitignore
index 17190f85..f7038fb9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,7 +4,7 @@
/sdk/gradle.properties
/.idea
.DS_Store
-/build
+build
/captures
.externalNativeBuild
.cxx
diff --git a/MAINTAINERS.md b/MAINTAINERS.md
index 8a5a0ea7..35f2db43 100644
--- a/MAINTAINERS.md
+++ b/MAINTAINERS.md
@@ -50,9 +50,9 @@ with:
```groovy
dependencies {
// ...
- implementation "com.github.hCaptcha:hcaptcha-android-sdk:BRANCH_NAME-SNAPSHOT"
+ implementation "com.github.hCaptcha.hcaptcha-android-sdk:sdk:BRANCH_NAME-SNAPSHOT"
// or
- implementation "com.github.hCaptcha:hcaptcha-android-sdk:pull/PR_NUMBER/head-SNAPSHOT"
+ implementation "com.github.hCaptcha.hcaptcha-android-sdk:sdk:pull/PR_NUMBER/head-SNAPSHOT"
}
```
1. Build `example-app` for `release` variant
diff --git a/README.md b/README.md
index 6f3e4319..210710df 100644
--- a/README.md
+++ b/README.md
@@ -18,12 +18,28 @@ repositories {
}
// Add hCaptcha sdk dependency inside the app's build.gradle file
dependencies {
- implementation 'com.github.hcaptcha:hcaptcha-android-sdk:x.y.z'
+ // For Android View
+ implementation 'com.github.hCaptcha.hcaptcha-android-sdk:sdk:x.y.z'
+ // For Jetpack Compose
+ implementation 'com.github.hCaptcha.hcaptcha-android-sdk:compose-sdk:x.y.z'
}
*Note: replace `x.y.z` with one from [Release](https://github.com/hCaptcha/hcaptcha-android-sdk/releases) (e.g. `1.0.0`).*
+### Legacy (versions < 5.0)
+
+
+// Register JitPack Repository inside the root build.gradle file
+repositories {
+ maven { url 'https://jitpack.io' }
+}
+// Add hCaptcha sdk dependency inside the app's build.gradle file
+dependencies {
+ implementation 'com.github.hcaptcha:hcaptcha-android-sdk:x.y.z'
+}
+
+
## Requirements
| Platform | Requirements |
diff --git a/benchmark/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java b/benchmark/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java
index b58cc9bc..af813916 100644
--- a/benchmark/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java
+++ b/benchmark/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java
@@ -56,7 +56,6 @@ public void onLoaded() {
latch.countDown();
}
},
- new TestHCaptchaStateListener(),
webView
);
});
diff --git a/build.gradle b/build.gradle
index 292133d2..77c8e0a8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,12 +3,12 @@ buildscript {
repositories {
google()
mavenCentral()
- maven { url 'https://jitpack.io' }
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.1.3'
- classpath 'androidx.benchmark:benchmark-gradle-plugin:1.2.0'
- classpath 'com.slack.keeper:keeper:0.15.0'
+ classpath 'com.android.tools.build:gradle:8.1.4'
+ classpath 'androidx.benchmark:benchmark-gradle-plugin:1.2.4'
+ classpath 'com.slack.keeper:keeper:0.16.0'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@@ -16,6 +16,7 @@ allprojects {
repositories {
google()
mavenCentral()
+ maven { url 'https://jitpack.io' }
}
}
diff --git a/compose-sdk/.gitignore b/compose-sdk/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/compose-sdk/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/compose-sdk/build.gradle b/compose-sdk/build.gradle
new file mode 100644
index 00000000..0fae4066
--- /dev/null
+++ b/compose-sdk/build.gradle
@@ -0,0 +1,103 @@
+plugins {
+ id 'maven-publish'
+ id 'com.android.library'
+ id 'org.jetbrains.kotlin.android'
+ id "pmd"
+ id "jacoco"
+ id "checkstyle"
+ id "com.github.spotbugs" version "5.2.3"
+ id "org.owasp.dependencycheck" version "7.1.1"
+ id "org.sonarqube" version "3.4.0.2513"
+}
+
+android {
+ namespace 'com.hcaptcha.compose'
+ compileSdk 34
+
+ defaultConfig {
+ minSdk 23
+
+ // See https://developer.android.com/studio/publish/versioning
+ // versionCode must be integer and be incremented by one for every new update
+ // android system uses this to prevent downgrades
+ versionCode 1
+
+ // version number visible to the user
+ // should follow semantic versioning (See https://semver.org)
+ versionName "0.1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_1_8
+ }
+
+ buildFeatures { // Enables Jetpack Compose for this module
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = "$compose_version"
+ }
+
+ publishing {
+ singleVariant('release') {
+ withSourcesJar()
+ withJavadocJar()
+ }
+ }
+}
+
+dependencies {
+ api project(':sdk')
+ implementation "androidx.compose.foundation:foundation:$compose_version"
+}
+
+project.afterEvaluate {
+ publishing {
+ repositories {
+ }
+
+ publications {
+ release(MavenPublication) {
+ from components.release
+
+ groupId = 'com.hcaptcha'
+ artifactId = 'compose-sdk'
+ version = android.defaultConfig.versionName
+
+ pom {
+ name = 'Android Jetpack Compose SDK hCaptcha'
+ description = 'This SDK provides a wrapper for hCaptcha and ready to use Jetpack Compose Component'
+ url = 'https://github.com/hCaptcha/hcaptcha-jetpack-compose'
+ licenses {
+ license {
+ name = 'MIT License'
+ url = 'https://github.com/hCaptcha/hcaptcha-jetpack-compose-sdk/blob/main/LICENSE'
+ }
+ }
+ scm {
+ connection = 'scm:git:git://github.com/hCaptcha/hcaptcha-android-sdk.git'
+ developerConnection = 'scm:git:ssh://github.com:hCaptcha/hcaptcha-android-sdk.git'
+ url = 'https://github.com/hCaptcha/hcaptcha-android-sdk'
+ }
+ }
+ }
+ }
+ }
+}
+
+apply from: "$rootProject.projectDir/gradle/shared/code-quality.gradle"
\ No newline at end of file
diff --git a/compose-sdk/consumer-rules.pro b/compose-sdk/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/compose-sdk/proguard-rules.pro b/compose-sdk/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/compose-sdk/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/compose-sdk/src/main/AndroidManifest.xml b/compose-sdk/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/compose-sdk/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/compose-sdk/src/main/java/com/hcaptcha/sdk/HCaptchaCompose.kt b/compose-sdk/src/main/java/com/hcaptcha/sdk/HCaptchaCompose.kt
new file mode 100644
index 00000000..6712c18f
--- /dev/null
+++ b/compose-sdk/src/main/java/com/hcaptcha/sdk/HCaptchaCompose.kt
@@ -0,0 +1,60 @@
+package com.hcaptcha.sdk
+
+import android.app.Activity
+import android.os.Handler
+import android.os.Looper
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+
+@Composable
+public fun HCaptchaCompose(config: HCaptchaConfig, onResult: (HCaptchaResponse) -> Unit) {
+ val handler = Handler(Looper.getMainLooper())
+ val verifier = object : IHCaptchaVerifier {
+ override fun onLoaded() {
+ onResult(HCaptchaResponse.Event(HCaptchaEvent.Loaded))
+ }
+
+ override fun onOpen() {
+ onResult(HCaptchaResponse.Event(HCaptchaEvent.Opened))
+ }
+
+ override fun onSuccess(result: String) {
+ onResult(HCaptchaResponse.Success(result))
+ }
+
+ override fun onFailure(exception: HCaptchaException) {
+ onResult(HCaptchaResponse.Failure(exception.hCaptchaError))
+ }
+
+ override fun startVerification(activity: Activity) {
+ error("startVerification should never be reached")
+ }
+
+ override fun reset() {
+ error("reset should never be reached")
+ }
+ }
+ val internalConfig = HCaptchaInternalConfig(com.hcaptcha.sdk.HCaptchaHtml())
+
+ Dialog(onDismissRequest = {}, properties = DialogProperties(usePlatformDefaultWidth = false)) {
+ AndroidView(
+ modifier = Modifier.fillMaxSize(),
+ factory = { context ->
+ HCaptchaWebView(context).apply {
+ HCaptchaWebViewHelper(
+ handler,
+ context,
+ config,
+ internalConfig,
+ verifier,
+ this
+ )
+ }
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/compose-sdk/src/main/java/com/hcaptcha/sdk/HCaptchaResponse.kt b/compose-sdk/src/main/java/com/hcaptcha/sdk/HCaptchaResponse.kt
new file mode 100644
index 00000000..45986316
--- /dev/null
+++ b/compose-sdk/src/main/java/com/hcaptcha/sdk/HCaptchaResponse.kt
@@ -0,0 +1,11 @@
+package com.hcaptcha.sdk
+
+enum class HCaptchaEvent {
+ Loaded,
+ Opened
+}
+sealed class HCaptchaResponse {
+ data class Success(val token: String) : HCaptchaResponse()
+ data class Failure(val error: HCaptchaError) : HCaptchaResponse()
+ data class Event(val event: HCaptchaEvent) : HCaptchaResponse()
+}
diff --git a/compose-sdk/src/test/.keep b/compose-sdk/src/test/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/example-app/build.gradle b/example-app/build.gradle
index fbc7f607..370a8016 100644
--- a/example-app/build.gradle
+++ b/example-app/build.gradle
@@ -1,4 +1,6 @@
-apply plugin: 'com.android.application'
+plugins {
+ id 'com.android.application'
+}
def intProp(name, fallback) {
return project.hasProperty(name) ? Integer.parseInt(project.getProperty(name)) : fallback
@@ -13,7 +15,7 @@ android {
namespace 'com.hcaptcha.example'
defaultConfig {
- minSdkVersion 16
+ minSdkVersion 23
targetSdkVersion intProp("exampleTargetSdkVersion", 34)
versionCode 1
versionName "0.0.1"
@@ -41,10 +43,11 @@ android {
}
dependencies {
+ implementation project(path: ':sdk')
+
//noinspection GradleDependency
implementation "androidx.appcompat:appcompat:${prop('exampleAppcompatVersion', '1.3.1')}"
implementation "com.google.android.flexbox:flexbox:3.0.0"
- implementation project(path: ':sdk')
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
diff --git a/example-app/src/main/AndroidManifest.xml b/example-app/src/main/AndroidManifest.xml
index 1c74abf1..55743545 100644
--- a/example-app/src/main/AndroidManifest.xml
+++ b/example-app/src/main/AndroidManifest.xml
@@ -24,6 +24,7 @@
+
\ No newline at end of file
diff --git a/example-app/src/main/res/values/strings.xml b/example-app/src/main/res/values/strings.xml
index cb3d4816..d64cbbe7 100644
--- a/example-app/src/main/res/values/strings.xml
+++ b/example-app/src/main/res/values/strings.xml
@@ -1,5 +1,5 @@
- Example hCaptcha App
+ hCaptcha Example
Reset
Setup
Verify
diff --git a/example-compose-app/build.gradle b/example-compose-app/build.gradle
new file mode 100644
index 00000000..8f19ed95
--- /dev/null
+++ b/example-compose-app/build.gradle
@@ -0,0 +1,60 @@
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
+
+def intProp(name, fallback) {
+ return project.hasProperty(name) ? Integer.parseInt(project.getProperty(name)) : fallback
+}
+
+android {
+ compileSdk intProp("exampleCompileSdkVersion", 34)
+ namespace 'com.hcaptcha.example.compose'
+
+ defaultConfig {
+ minSdkVersion 23
+ targetSdkVersion intProp("exampleTargetSdkVersion", 34)
+ versionCode 1
+ versionName "0.0.1"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_1_8
+ }
+
+ buildTypes {
+ release {
+ signingConfig signingConfigs.debug
+ minifyEnabled true
+ }
+ }
+
+ lint {
+ disable 'UsingOnClickInXml'
+ }
+
+ buildFeatures { // Enables Jetpack Compose for this module
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = "$compose_version"
+ }
+}
+
+dependencies {
+ implementation project(path: ':compose-sdk')
+
+ implementation "androidx.compose.ui:ui:$compose_version"
+ implementation 'androidx.compose.material3:material3:1.2.1'
+ implementation 'androidx.activity:activity-ktx:1.8.2'
+ implementation 'androidx.activity:activity-compose:1.8.2'
+ implementation "androidx.compose.foundation:foundation-layout-android:$compose_version"
+
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+}
diff --git a/example-compose-app/src/main/AndroidManifest.xml b/example-compose-app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..fed67a0c
--- /dev/null
+++ b/example-compose-app/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/example-compose-app/src/main/java/com/hcaptcha/example/compose/ComposeActivity.kt b/example-compose-app/src/main/java/com/hcaptcha/example/compose/ComposeActivity.kt
new file mode 100644
index 00000000..95fb285c
--- /dev/null
+++ b/example-compose-app/src/main/java/com/hcaptcha/example/compose/ComposeActivity.kt
@@ -0,0 +1,103 @@
+package com.hcaptcha.example.compose
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.runtime.*
+import androidx.compose.material3.Button
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.hcaptcha.sdk.HCaptchaCompose
+import com.hcaptcha.sdk.HCaptchaConfig
+import com.hcaptcha.sdk.HCaptchaEvent
+import com.hcaptcha.sdk.HCaptchaResponse
+
+class ComposeActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ var hCaptchaStarted by remember { mutableStateOf(false) }
+ var hCaptchaLoaded by remember { mutableStateOf(false) }
+ var text by remember { mutableStateOf("") }
+
+ Column(
+ modifier = Modifier.fillMaxSize().padding(16.dp),
+ verticalArrangement = Arrangement.Bottom
+ ) {
+ // Multiline Text
+ TextField(
+ value = text,
+ onValueChange = { newText -> text = newText },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp)
+ .background(Color.Gray)
+ )
+
+ Button(
+ onClick = {
+ hCaptchaStarted = !hCaptchaStarted
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp)
+ ) {
+ Text(text = "Toggle WebView")
+ }
+
+ if (hCaptchaStarted && !hCaptchaLoaded) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ CircularProgressIndicator(
+ modifier = Modifier.width(64.dp),
+ color = MaterialTheme.colorScheme.secondary,
+ trackColor = MaterialTheme.colorScheme.surfaceVariant,
+ )
+ }
+ }
+
+ // WebView Dialog
+ if (hCaptchaStarted) {
+ HCaptchaCompose(HCaptchaConfig
+ .builder()
+ .siteKey("10000000-ffff-ffff-ffff-000000000001")
+ .build()) { result ->
+ when (result) {
+ is HCaptchaResponse.Success -> {
+ text = "Success: ${result.token}"
+ hCaptchaStarted = false
+ hCaptchaLoaded = false
+ println(text)
+ }
+ is HCaptchaResponse.Failure -> {
+ hCaptchaStarted = false
+ hCaptchaLoaded = false
+ text = "Failure: ${result.error.message}"
+ println(text)
+ }
+ is HCaptchaResponse.Event -> {
+ if (result.event == HCaptchaEvent.Opened) {
+ hCaptchaLoaded = true;
+ }
+ println("Event: ${result.event}")
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/example-compose-app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/example-compose-app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..a1c43bca
--- /dev/null
+++ b/example-compose-app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/example-compose-app/src/main/res/drawable/ic_launcher_background.xml b/example-compose-app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..972ff98c
--- /dev/null
+++ b/example-compose-app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,224 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example-compose-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/example-compose-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..6f3b755b
--- /dev/null
+++ b/example-compose-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/example-compose-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/example-compose-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..6f3b755b
--- /dev/null
+++ b/example-compose-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/example-compose-app/src/main/res/mipmap-hdpi/ic_launcher.webp b/example-compose-app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 00000000..c209e78e
Binary files /dev/null and b/example-compose-app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/example-compose-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/example-compose-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..b2dfe3d1
Binary files /dev/null and b/example-compose-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/example-compose-app/src/main/res/mipmap-mdpi/ic_launcher.webp b/example-compose-app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 00000000..4f0f1d64
Binary files /dev/null and b/example-compose-app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/example-compose-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/example-compose-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..62b611da
Binary files /dev/null and b/example-compose-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/example-compose-app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/example-compose-app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 00000000..948a3070
Binary files /dev/null and b/example-compose-app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/example-compose-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/example-compose-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..1b9a6956
Binary files /dev/null and b/example-compose-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/example-compose-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/example-compose-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..28d4b77f
Binary files /dev/null and b/example-compose-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/example-compose-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/example-compose-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9287f508
Binary files /dev/null and b/example-compose-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/example-compose-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/example-compose-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..aa7d6427
Binary files /dev/null and b/example-compose-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/example-compose-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/example-compose-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9126ae37
Binary files /dev/null and b/example-compose-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/example-compose-app/src/main/res/values/strings.xml b/example-compose-app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..5724064d
--- /dev/null
+++ b/example-compose-app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ hCaptcha Compose
+
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 2b1e584a..2201abbc 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -18,4 +18,7 @@ android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# To test more aggressive optimizations
-android.enableR8.fullMode=true
\ No newline at end of file
+android.enableR8.fullMode=true
+# Kotline version
+kotlin_version=1.9.10
+compose_version=1.5.3
\ No newline at end of file
diff --git a/gradle/shared/code-quality.gradle b/gradle/shared/code-quality.gradle
new file mode 100644
index 00000000..efe77442
--- /dev/null
+++ b/gradle/shared/code-quality.gradle
@@ -0,0 +1,108 @@
+checkstyle {
+ toolVersion = '8.45.1'
+}
+
+task checkstyle(type: Checkstyle) {
+ description 'Check code standard'
+ group 'verification'
+ configFile file("${rootDir}/gradle/config/checkstyle.xml")
+ source 'src'
+ include '**/*.java'
+ exclude '**/gen/**'
+ classpath = files()
+ ignoreFailures = false
+ maxWarnings = 0
+}
+
+pmd {
+ consoleOutput = true
+ toolVersion = "6.51.0"
+}
+
+task pmd(type: Pmd) {
+ ruleSetFiles = files("${project.rootDir}/gradle/config/pmd.xml")
+ ignoreFailures = false
+ ruleSets = []
+ source 'src'
+ include '**/*.java'
+ exclude '**/gen/**'
+ reports {
+ xml.required = false
+ xml.outputLocation = file("${project.buildDir}/reports/pmd/pmd.xml")
+ html.required = true
+ html.outputLocation = file("$project.buildDir/outputs/pmd/pmd.html")
+ }
+}
+
+spotbugs {
+ ignoreFailures = false
+ showStackTraces = true
+ showProgress = false
+ reportLevel = 'high'
+ excludeFilter = file("${project.rootDir}/gradle/config/findbugs-exclude.xml")
+ onlyAnalyze = ['com.hcaptcha.sdk.*']
+ projectName = name
+ release = version
+}
+
+// enable html report
+gradle.taskGraph.beforeTask { task ->
+ if (task.name.toLowerCase().contains('spotbugs')) {
+ task.reports {
+ html.enabled true
+ xml.enabled true
+ }
+ }
+}
+
+// https://www.rallyhealth.com/coding/code-coverage-for-android-testing
+task jacocoUnitTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) {
+ def coverageSourceDirs = [
+ "src/main/java"
+ ]
+ def javaClasses = fileTree(
+ dir: "${project.buildDir}/intermediates/javac/debug/classes",
+ excludes: [
+ '**/R.class',
+ '**/R$*.class',
+ '**/BuildConfig.*',
+ '**/Manifest*.*'
+ ]
+ )
+
+ classDirectories.from files([javaClasses])
+ additionalSourceDirs.from files(coverageSourceDirs)
+ sourceDirectories.from files(coverageSourceDirs)
+ executionData.from = "${project.buildDir}/jacoco/testDebugUnitTest.exec"
+
+ reports {
+ xml.required = true
+ html.required = true
+ }
+}
+
+check.dependsOn('checkstyle', 'pmd', 'jacocoUnitTestReport')
+
+sonarqube {
+ properties {
+ property "sonar.projectKey", "hCaptcha_hcaptcha-android-sdk"
+ property "sonar.organization", "hcaptcha"
+ property "sonar.host.url", "https://sonarcloud.io"
+
+ property "sonar.language", "java"
+ property "sonar.sourceEncoding", "utf-8"
+
+ property "sonar.sources", "src/main"
+ property "sonar.java.binaries", "${project.buildDir}/intermediates/javac/debug/classes"
+ property "sonar.tests", ["src/test/", "../test/src/androidTest/"]
+
+ property "sonar.android.lint.report", "${project.buildDir}/outputs/lint-results.xml"
+ property "sonar.java.spotbugs.reportPaths", ["${project.buildDir}/reports/spotbugs/debug.xml", "${project.buildDir}/reports/spotbugs/release.xml"]
+ property "sonar.java.pmd.reportPaths", "${project.buildDir}/reports/pmd/pmd.xml"
+ property "sonar.java.checkstyle.reportPaths", "${project.buildDir}/reports/checkstyle/checkstyle.xml"
+ property "sonar.coverage.jacoco.xmlReportPaths", "${project.buildDir}/reports/jacoco/jacocoUnitTestReport.xml"
+ }
+}
+
+project.tasks["sonarqube"].dependsOn "check"
+
diff --git a/sdk/build.gradle b/sdk/build.gradle
index 95229952..f78c5ddc 100644
--- a/sdk/build.gradle
+++ b/sdk/build.gradle
@@ -1,9 +1,9 @@
plugins {
id "com.android.library"
+ id "maven-publish"
id "pmd"
id "jacoco"
id "checkstyle"
- id "maven-publish"
id "com.github.spotbugs" version "5.2.3"
id "org.owasp.dependencycheck" version "7.1.1"
id "org.sonarqube" version "3.4.0.2513"
@@ -100,15 +100,6 @@ project.afterEvaluate {
url = 'https://github.com/hCaptcha/hcaptcha-android-sdk/blob/main/LICENSE'
}
}
- developers {
- developer {
- id = 'sergiu'
- name = 'Sergiu Danalachi'
- email = 'sergiu@intuitionmachines.com'
- organization = 'hCaptcha'
- organizationUrl = 'https://www.hcaptcha.com'
- }
- }
scm {
connection = 'scm:git:git://github.com/hCaptcha/hcaptcha-android-sdk.git'
developerConnection = 'scm:git:ssh://github.com:hCaptcha/hcaptcha-android-sdk.git'
@@ -180,111 +171,4 @@ android.libraryVariants.all { variant ->
variant.registerJavaGeneratingTask(generateTask, outputDir)
}
-checkstyle {
- toolVersion = '8.45.1'
-}
-
-task checkstyle(type: Checkstyle) {
- description 'Check code standard'
- group 'verification'
- configFile file("${rootDir}/gradle/config/checkstyle.xml")
- source 'src'
- include '**/*.java'
- exclude '**/gen/**'
- classpath = files()
- ignoreFailures = false
- maxWarnings = 0
-}
-
-pmd {
- consoleOutput = true
- toolVersion = "6.51.0"
-}
-
-task pmd(type: Pmd) {
- ruleSetFiles = files("${project.rootDir}/gradle/config/pmd.xml")
- ignoreFailures = false
- ruleSets = []
- source 'src'
- include '**/*.java'
- exclude '**/gen/**'
- reports {
- xml.required = false
- xml.outputLocation = file("${project.buildDir}/reports/pmd/pmd.xml")
- html.required = true
- html.outputLocation = file("$project.buildDir/outputs/pmd/pmd.html")
- }
-}
-
-spotbugs {
- ignoreFailures = false
- showStackTraces = true
- showProgress = false
- reportLevel = 'high'
- excludeFilter = file("${project.rootDir}/gradle/config/findbugs-exclude.xml")
- onlyAnalyze = ['com.hcaptcha.sdk.*']
- projectName = name
- release = version
-}
-
-// enable html report
-gradle.taskGraph.beforeTask { task ->
- if (task.name.toLowerCase().contains('spotbugs')) {
- task.reports {
- html.enabled true
- xml.enabled true
- }
- }
-}
-
-// https://www.rallyhealth.com/coding/code-coverage-for-android-testing
-task jacocoUnitTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) {
- def coverageSourceDirs = [
- "src/main/java"
- ]
- def javaClasses = fileTree(
- dir: "${project.buildDir}/intermediates/javac/debug/classes",
- excludes: [
- '**/R.class',
- '**/R$*.class',
- '**/BuildConfig.*',
- '**/Manifest*.*'
- ]
- )
-
- classDirectories.from files([javaClasses])
- additionalSourceDirs.from files(coverageSourceDirs)
- sourceDirectories.from files(coverageSourceDirs)
- executionData.from = "${project.buildDir}/jacoco/testDebugUnitTest.exec"
-
- reports {
- xml.required = true
- html.required = true
- }
-}
-
-check.dependsOn('checkstyle', 'pmd', 'jacocoUnitTestReport')
-
-sonarqube {
- properties {
- property "sonar.projectKey", "hCaptcha_hcaptcha-android-sdk"
- property "sonar.organization", "hcaptcha"
- property "sonar.host.url", "https://sonarcloud.io"
-
- property "sonar.language", "java"
- property "sonar.sourceEncoding", "utf-8"
-
- property "sonar.sources", "src/main"
- property "sonar.java.binaries", "${project.buildDir}/intermediates/javac/debug/classes"
- property "sonar.tests", ["src/test/", "../test/src/androidTest/"]
-
- property "sonar.android.lint.report", "${project.buildDir}/outputs/lint-results.xml"
- property "sonar.java.spotbugs.reportPaths", ["${project.buildDir}/reports/spotbugs/debug.xml", "${project.buildDir}/reports/spotbugs/release.xml"]
- property "sonar.java.pmd.reportPaths", "${project.buildDir}/reports/pmd/pmd.xml"
- property "sonar.java.checkstyle.reportPaths", "${project.buildDir}/reports/checkstyle/checkstyle.xml"
- property "sonar.coverage.jacoco.xmlReportPaths", "${project.buildDir}/reports/jacoco/jacocoUnitTestReport.xml"
- }
-}
-
-project.tasks["sonarqube"].dependsOn "check"
-
+apply from: "$rootProject.projectDir/gradle/shared/code-quality.gradle"
\ No newline at end of file
diff --git a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java
index 18f8b518..c964ccf7 100644
--- a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java
+++ b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java
@@ -56,6 +56,9 @@ public final class HCaptchaDialogFragment extends DialogFragment implements IHCa
@Nullable
private HCaptchaWebViewHelper webViewHelper;
+ @NonNull
+ private HCaptchaStateListener listener;
+
private LinearLayout loadingContainer;
private float defaultDimAmount = 0.6f;
@@ -95,7 +98,6 @@ public View onCreateView(@Nullable LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable Bundle savedInstanceState) {
HCaptchaLog.d("DialogFragment.onCreateView");
- HCaptchaStateListener listener = null;
try {
final Bundle args = getArguments();
listener = HCaptchaCompat.getParcelable(args, KEY_LISTENER, HCaptchaStateListener.class);
@@ -122,7 +124,7 @@ public View onCreateView(@Nullable LayoutInflater inflater,
loadingContainer.setVisibility(Boolean.TRUE.equals(config.getLoading()) ? View.VISIBLE : View.GONE);
webViewHelper = new HCaptchaWebViewHelper(new Handler(Looper.getMainLooper()),
- requireContext(), config, internalConfig, this, listener, webView);
+ requireContext(), config, internalConfig, this, webView);
readyForInteraction = false;
return rootView;
} catch (AssertionError | BadParcelableException | InflateException | ClassCastException e) {
@@ -211,7 +213,7 @@ public void onOpen() {
readyForInteraction = true;
- webViewHelper.getListener().onOpen();
+ listener.onOpen();
}
@Override
@@ -224,7 +226,7 @@ public void onFailure(@NonNull final HCaptchaException exception) {
if (silentRetry) {
webViewHelper.resetAndExecute();
} else {
- webViewHelper.getListener().onFailure(exception);
+ listener.onFailure(exception);
}
}
}
@@ -235,7 +237,7 @@ public void onSuccess(final String token) {
if (isAdded()) {
dismissAllowingStateLoss();
}
- webViewHelper.getListener().onSuccess(token);
+ listener.onSuccess(token);
}
@Override
@@ -254,7 +256,7 @@ public void startVerification(@NonNull Activity fragmentActivity) {
// https://stackoverflow.com/q/14262312/902217
// Happens if Fragment is stopped i.e. activity is about to destroy on show call
if (webViewHelper != null) {
- webViewHelper.getListener().onFailure(new HCaptchaException(HCaptchaError.ERROR));
+ listener.onFailure(new HCaptchaException(HCaptchaError.ERROR));
}
}
}
diff --git a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaHeadlessWebView.java b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaHeadlessWebView.java
index 4302754e..60cd0a60 100644
--- a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaHeadlessWebView.java
+++ b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaHeadlessWebView.java
@@ -40,7 +40,7 @@ final class HCaptchaHeadlessWebView implements IHCaptchaVerifier {
rootView.addView(webView);
}
webViewHelper = new HCaptchaWebViewHelper(
- new Handler(Looper.getMainLooper()), activity, config, internalConfig, this, listener, webView);
+ new Handler(Looper.getMainLooper()), activity, config, internalConfig, this, webView);
}
@Override
diff --git a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaWebViewHelper.java b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaWebViewHelper.java
index 17096617..e86b58c3 100644
--- a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaWebViewHelper.java
+++ b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaWebViewHelper.java
@@ -33,10 +33,6 @@ final class HCaptchaWebViewHelper {
@NonNull
private final IHCaptchaVerifier captchaVerifier;
- @Getter
- @NonNull
- private final HCaptchaStateListener listener;
-
@Getter
@NonNull
private final HCaptchaWebView webView;
@@ -49,12 +45,10 @@ final class HCaptchaWebViewHelper {
@NonNull final HCaptchaConfig config,
@NonNull final HCaptchaInternalConfig internalConfig,
@NonNull final IHCaptchaVerifier captchaVerifier,
- @NonNull final HCaptchaStateListener listener,
@NonNull final HCaptchaWebView webView) {
this.context = context;
this.config = config;
this.captchaVerifier = captchaVerifier;
- this.listener = listener;
this.webView = webView;
this.htmlProvider = internalConfig.getHtmlProvider();
setupWebView(handler);
@@ -79,7 +73,7 @@ private void setupWebView(@NonNull final Handler handler) {
settings.setAllowFileAccess(false);
settings.setAllowContentAccess(false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- webView.setWebViewClient(new HCaptchaWebClient(handler, listener));
+ webView.setWebViewClient(new HCaptchaWebClient(handler));
}
if (HCaptchaLog.sDiagnosticsLogEnabled) {
webView.setWebChromeClient(new HCaptchaWebChromeClient());
@@ -130,12 +124,8 @@ private class HCaptchaWebClient extends WebViewClient {
@NonNull
private final Handler handler;
- @NonNull
- private final HCaptchaStateListener listener;
-
- HCaptchaWebClient(@NonNull Handler handler, @NonNull HCaptchaStateListener listener) {
+ HCaptchaWebClient(@NonNull Handler handler) {
this.handler = handler;
- this.listener = listener;
}
private String stripUrl(String url) {
@@ -149,7 +139,7 @@ public WebResourceResponse shouldInterceptRequest (final WebView view, final Web
handler.post(() -> {
webView.removeJavascriptInterface(HCaptchaJSInterface.JS_INTERFACE_TAG);
webView.removeJavascriptInterface(HCaptchaDebugInfo.JS_INTERFACE_TAG);
- listener.onFailure(new HCaptchaException(HCaptchaError.INSECURE_HTTP_REQUEST_ERROR,
+ captchaVerifier.onFailure(new HCaptchaException(HCaptchaError.INSECURE_HTTP_REQUEST_ERROR,
"Insecure resource " + requestUri + " requested"));
});
}
diff --git a/sdk/src/test/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java b/sdk/src/test/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java
index dd7458b9..18b2c1f2 100644
--- a/sdk/src/test/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java
+++ b/sdk/src/test/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java
@@ -41,9 +41,6 @@ public class HCaptchaWebViewHelperTest {
@Mock
IHCaptchaVerifier captchaVerifier;
- @Mock
- HCaptchaStateListener stateListener;
-
@Mock
HCaptchaWebView webView;
@@ -62,7 +59,6 @@ public class HCaptchaWebViewHelperTest {
public void init() {
MockitoAnnotations.openMocks(this);
androidLogMock = mockStatic(Log.class);
- stateListener = mock(HCaptchaStateListener.class);
webView = mock(HCaptchaWebView.class);
webSettings = mock(WebSettings.class);
htmlProvider = mock(IHCaptchaHtmlProvider.class);
@@ -79,7 +75,7 @@ public void release() {
@Test
public void test_constructor() {
new HCaptchaWebViewHelper(handler, context, config, internalConfig, captchaVerifier,
- stateListener, webView);
+ webView);
verify(webView).loadDataWithBaseURL(null, MOCK_HTML, "text/html", "UTF-8", null);
verify(webView, times(2)).addJavascriptInterface(any(), anyString());
}
@@ -87,7 +83,7 @@ public void test_constructor() {
@Test
public void test_destroy() {
final HCaptchaWebViewHelper webViewHelper = new HCaptchaWebViewHelper(handler, context, config,
- internalConfig, captchaVerifier, stateListener, webView);
+ internalConfig, captchaVerifier, webView);
final ViewGroup viewParent = mock(ViewGroup.class, withSettings().extraInterfaces(ViewParent.class));
when(webView.getParent()).thenReturn(viewParent);
webViewHelper.destroy();
@@ -98,7 +94,7 @@ public void test_destroy() {
@Test
public void test_destroy_webview_parent_null() {
final HCaptchaWebViewHelper webViewHelper = new HCaptchaWebViewHelper(handler, context, config,
- internalConfig, captchaVerifier, stateListener, webView);
+ internalConfig, captchaVerifier, webView);
webViewHelper.destroy();
}
@@ -106,8 +102,7 @@ public void test_destroy_webview_parent_null() {
public void test_config_host_pased() {
final String host = "https://my.awesome.host";
when(config.getHost()).thenReturn(host);
- new HCaptchaWebViewHelper(handler, context, config, internalConfig, captchaVerifier,
- stateListener, webView);
+ new HCaptchaWebViewHelper(handler, context, config, internalConfig, captchaVerifier, webView);
verify(webView).loadDataWithBaseURL(host, MOCK_HTML, "text/html", "UTF-8", null);
}
}
diff --git a/settings.gradle b/settings.gradle
index 892e2df7..5761715c 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -3,3 +3,5 @@ include ':sdk'
include ':test'
include ':benchmark'
include ':example-app'
+include ':compose-sdk'
+include ':example-compose-app'
diff --git a/test/build.gradle b/test/build.gradle
index 09be1038..c0df2679 100644
--- a/test/build.gradle
+++ b/test/build.gradle
@@ -1,4 +1,6 @@
apply plugin: 'com.android.application'
+apply plugin: 'org.jetbrains.kotlin.android'
+
if (project.hasProperty("testingMinimizedBuild")) {
apply plugin: 'com.slack.keeper'
}
@@ -9,7 +11,7 @@ android {
defaultConfig {
applicationId "com.hcaptcha.sdk.test"
- minSdkVersion 19
+ minSdkVersion 23
targetSdkVersion 34
versionCode 1
versionName "1.0"
@@ -31,10 +33,50 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
- testBuildType = project.hasProperty("testingMinimizedBuild") ? "release" : "debug"
+ testBuildType project.hasProperty("testingMinimizedBuild") ? "release" : "debug"
testOptions {
animationsDisabled = true
}
+
+ buildFeatures {
+ compose = true
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_1_8
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = "$compose_version"
+ }
+}
+
+if (project.hasProperty("testingMinimizedBuild")) {
+ project.afterEvaluate {
+ tasks.register("postInferReleaseAndroidTestKeepRulesForKeeper") {
+ doLast {
+ def sourceFile = file("${projectDir}/test-proguard-rules.pro")
+ def destinationFile = fileTree(dir: "${project.buildDir}/intermediates/keeper", include: '**/inferredKeepRules.pro').find { true }
+
+ if (sourceFile.exists() && destinationFile.exists()) {
+ def sourceText = sourceFile.text
+ destinationFile << sourceText
+ println("Rules from of ${sourceFile} appended too keeper")
+ } else {
+ if (!sourceFile.exists()) {
+ throw new GradleException("Proguard file does not exist: ${sourceFile}")
+ }
+ if (!destinationFile.exists()) {
+ throw new GradleException("Keeper's proguard file does not exist: ${destinationFile}")
+ }
+ }
+ }
+ }
+
+ tasks.named("inferReleaseAndroidTestKeepRulesForKeeper").configure {
+ finalizedBy(tasks.named("postInferReleaseAndroidTestKeepRulesForKeeper"))
+ }
+ }
}
androidComponents {
@@ -48,11 +90,10 @@ androidComponents {
}
dependencies {
- implementation project(path: ':sdk')
- implementation 'androidx.appcompat:appcompat:1.6.1'
-
testImplementation 'junit:junit:4.13.2'
+ implementation project(path: ':sdk')
+ implementation 'androidx.appcompat:appcompat:1.6.1'
androidTestImplementation 'androidx.fragment:fragment-testing:1.6.2'
androidTestImplementation 'androidx.test:core:1.5.0'
androidTestImplementation 'androidx.test:rules:1.5.0'
@@ -61,4 +102,10 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-web:3.5.1'
androidTestImplementation 'org.mockito:mockito-android:5.3.1'
+
+ implementation project(path: ':compose-sdk')
+ implementation 'androidx.compose.material3:material3:1.2.1'
+ implementation "androidx.compose.ui:ui:$compose_version"
+ implementation "androidx.compose.foundation:foundation-layout-android:$compose_version"
+ androidTestImplementation 'androidx.compose.ui:ui-test-junit4-android:1.6.8'
}
diff --git a/test/proguard-rules.pro b/test/proguard-rules.pro
deleted file mode 100644
index e23fa22e..00000000
--- a/test/proguard-rules.pro
+++ /dev/null
@@ -1 +0,0 @@
-# Proguard rules that are applied to your test apk/code.
diff --git a/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java b/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java
index fb7b1017..4501c9e3 100644
--- a/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java
+++ b/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java
@@ -5,6 +5,7 @@
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
+import android.app.Activity;
import android.os.Handler;
import android.os.Looper;
@@ -43,17 +44,35 @@ public void testInsecureHttpRequestErrorHandling() throws Exception {
final Handler handler = new Handler(Looper.getMainLooper());
final CountDownLatch failureLatch = new CountDownLatch(1);
final HCaptchaConfig config = baseConfig.toBuilder().host("http://localhost").build();
- final IHCaptchaVerifier verifier = mock(IHCaptchaVerifier.class);
- final HCaptchaStateListener listener = new HCaptchaStateTestAdapter() {
+ final IHCaptchaVerifier verifier = new IHCaptchaVerifier() {
@Override
- void onSuccess(String token) {
+ public void onOpen() {
failAsNonReachable();
}
@Override
- void onFailure(HCaptchaException e) {
+ public void onLoaded() {
+ }
+
+ @Override
+ public void startVerification(Activity activity) {
+ failAsNonReachable();
+ }
+
+ @Override
+ public void reset() {
+ failAsNonReachable();
+ }
+
+ @Override
+ public void onSuccess(String token) {
+ failAsNonReachable();
+ }
+
+ @Override
+ public void onFailure(HCaptchaException e) {
assertEquals(HCaptchaError.INSECURE_HTTP_REQUEST_ERROR, e.getHCaptchaError());
assertEquals("Insecure resource http://localhost/favicon.ico requested", e.getMessage());
failureLatch.countDown();
@@ -64,7 +83,7 @@ void onFailure(HCaptchaException e) {
scenario.onActivity(activity -> {
HCaptchaWebView webView = new HCaptchaWebView(activity);
final HCaptchaWebViewHelper helper = new HCaptchaWebViewHelper(
- handler, activity, config, internalConfig, verifier, listener, webView);
+ handler, activity, config, internalConfig, verifier, webView);
});
assertTrue(failureLatch.await(AWAIT_CALLBACK_MS, TimeUnit.MILLISECONDS));
diff --git a/test/src/androidTest/java/com/hcaptcha/sdk/compose/HCaptchaComposeTest.kt b/test/src/androidTest/java/com/hcaptcha/sdk/compose/HCaptchaComposeTest.kt
new file mode 100644
index 00000000..5f44de99
--- /dev/null
+++ b/test/src/androidTest/java/com/hcaptcha/sdk/compose/HCaptchaComposeTest.kt
@@ -0,0 +1,78 @@
+package com.hcaptcha.sdk.compose
+
+import androidx.compose.ui.test.*
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.Text
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.hcaptcha.sdk.HCaptchaCompose
+import com.hcaptcha.sdk.HCaptchaConfig
+import com.hcaptcha.sdk.HCaptchaError
+import com.hcaptcha.sdk.HCaptchaResponse
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.TimeUnit
+
+@RunWith(AndroidJUnit4::class)
+class HCaptchaComposeTest {
+ private val resultContentDescription = "HCaptchaResultString"
+ private val timeout = TimeUnit.SECONDS.toMillis(4)
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ fun setContent(token: String = "10000000-ffff-ffff-ffff-000000000001") {
+ composeTestRule.setContent {
+ var text by remember { mutableStateOf("") }
+ Column {
+ Text(text = text, modifier = Modifier.semantics { contentDescription = resultContentDescription })
+
+ HCaptchaCompose(HCaptchaConfig
+ .builder()
+ .siteKey(token)
+ .diagnosticLog(true)
+ .build()) { result ->
+ when (result) {
+ is HCaptchaResponse.Success -> {
+ text = result.token
+ }
+ is HCaptchaResponse.Failure -> {
+ text = result.error.name
+ }
+ else -> {}
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun validToken() {
+ setContent()
+
+ runBlocking { delay(timeout) }
+
+ composeTestRule.onNodeWithContentDescription(resultContentDescription)
+ .assertTextEquals("10000000-aaaa-bbbb-cccc-000000000001")
+ }
+
+ @Test
+ fun invalidToken() {
+ setContent("")
+
+ runBlocking { delay(timeout) }
+
+ composeTestRule.onNodeWithContentDescription(resultContentDescription)
+ .assertTextContains(HCaptchaError.ERROR.name)
+ }
+}
\ No newline at end of file
diff --git a/test/src/main/AndroidManifest.xml b/test/src/main/AndroidManifest.xml
index e6434d05..a9f4be7f 100644
--- a/test/src/main/AndroidManifest.xml
+++ b/test/src/main/AndroidManifest.xml
@@ -34,6 +34,8 @@
+
+
\ No newline at end of file
diff --git a/test/test-proguard-rules.pro b/test/test-proguard-rules.pro
new file mode 100644
index 00000000..b34cd962
--- /dev/null
+++ b/test/test-proguard-rules.pro
@@ -0,0 +1,3 @@
+# Proguard rules that are applied to your test apk/code.
+-keep class androidx.compose.ui.test.** { *; }
+-keep class androidx.compose.ui.platform.** { *; }