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.** { *; }