Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,7 @@ jobs:
test-benchmark:
name: 'Test Benchmark'
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
runs-on: macos-15
permissions:
pull-requests: write
contents: read
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class MainActivity extends AppCompatActivity {
private CheckBox loading;
private CheckBox disableHardwareAccel;
private CheckBox themeDark;
private CheckBox userJourney;
private TextView tokenTextView;
private TextView errorTextView;
private TextView phonePrefixInput;
Expand All @@ -45,6 +46,7 @@ protected void onCreate(Bundle savedInstanceState) {
loading = findViewById(R.id.loading);
disableHardwareAccel = findViewById(R.id.hwAccel);
themeDark = findViewById(R.id.themeDark);
userJourney = findViewById(R.id.userJourney);
phonePrefixInput = findViewById(R.id.phonePrefix);
phoneModeSwitch = findViewById(R.id.phoneModeSwitch);
rqdataInput = findViewById(R.id.rqdataInput);
Expand Down Expand Up @@ -88,6 +90,7 @@ private HCaptchaConfig getConfig() {
.hideDialog(hideDialog.isChecked())
.theme(isDark ? HCaptchaTheme.DARK : HCaptchaTheme.LIGHT)
.disableHardwareAcceleration(disableHardwareAccel.isChecked())
.userJourney(userJourney.isChecked())
.tokenExpiration(10)
.diagnosticLog(true)
.retryPredicate((config, exception) -> exception.getHCaptchaError() == HCaptchaError.SESSION_TIMEOUT)
Expand Down
8 changes: 8 additions & 0 deletions example-app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@
android:checked="false"
android:text="@string/theme_dark"
style="@style/CheckBoxText" />
<CheckBox
android:id="@+id/userJourney"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:checked="false"
android:text="@string/user_journey"
style="@style/CheckBoxText" />
</com.google.android.flexbox.FlexboxLayout>

<!-- Phone input row -->
Expand Down
1 change: 1 addition & 0 deletions example-app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
<string name="hide_dialog">Hide Dialog</string>
<string name="hit_test">Hit test</string>
<string name="theme_dark">Dark Theme</string>
<string name="user_journey">Journey</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.hcaptcha.sdk.HCaptchaConfig
import com.hcaptcha.sdk.HCaptchaEvent
import com.hcaptcha.sdk.HCaptchaResponse
import com.hcaptcha.sdk.HCaptchaSize
import com.hcaptcha.sdk.journeylitics.AnalyticsScreen

class ComposeActivity : ComponentActivity() {

Expand All @@ -29,59 +30,67 @@ class ComposeActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var hideDialog by remember { mutableStateOf(false) }
var captchaState by remember { mutableStateOf(CaptchaState.Idle) }
var text by remember { mutableStateOf("") }
AnalyticsScreen("ComposeActivity") {
var hideDialog by remember { mutableStateOf(false) }
var userJourney by remember { mutableStateOf(false) }
var captchaState by remember { mutableStateOf(CaptchaState.Idle) }
var text by remember { mutableStateOf("") }

val hCaptchaConfig = remember(hideDialog) {
HCaptchaConfig.builder()
.siteKey("10000000-ffff-ffff-ffff-000000000001")
.size(if (hideDialog) HCaptchaSize.INVISIBLE else HCaptchaSize.NORMAL)
.hideDialog(hideDialog)
.diagnosticLog(true)
.build()
}
val hCaptchaConfig = remember(hideDialog, userJourney) {
HCaptchaConfig.builder()
.siteKey("10000000-ffff-ffff-ffff-000000000001")
.size(if (hideDialog) HCaptchaSize.INVISIBLE else HCaptchaSize.NORMAL)
.hideDialog(hideDialog)
.userJourney(userJourney)
.diagnosticLog(true)
.build()
}

if (captchaState != CaptchaState.Idle) {
HCaptchaCompose(hCaptchaConfig) { result ->
val message = when (result) {
is HCaptchaResponse.Success -> {
captchaState = CaptchaState.Idle
"Success: ${result.token}"
}
is HCaptchaResponse.Failure -> {
captchaState = CaptchaState.Idle
"Failure: ${result.error.message}"
}
is HCaptchaResponse.Event -> {
if (result.event == HCaptchaEvent.Opened) {
captchaState = CaptchaState.Loaded
if (captchaState != CaptchaState.Idle) {
HCaptchaCompose(hCaptchaConfig) { result ->
val message = when (result) {
is HCaptchaResponse.Success -> {
captchaState = CaptchaState.Idle
"Success: ${result.token}"
}
is HCaptchaResponse.Failure -> {
captchaState = CaptchaState.Idle
"Failure: ${result.error.message}"
}
is HCaptchaResponse.Event -> {
if (result.event == HCaptchaEvent.Opened) {
captchaState = CaptchaState.Loaded
}
"Event: ${result.event}"
}
"Event: ${result.event}"
}
text += "\n${message}"
println(message)
}
text += "\n${message}"
println(message)
}
}

CaptchaControlUI(
hideDialog = hideDialog,
onHideDialogChanged = { hideDialog = it },
text = text,
onVerifyClick = {
captchaState = CaptchaState.Started
text = ""
},
showProgress = captchaState == CaptchaState.Started
)
CaptchaControlUI(
hideDialog = hideDialog,
onHideDialogChanged = { hideDialog = it },
userJourney = userJourney,
onUserJourneyChanged = { userJourney = it },
text = text,
onVerifyClick = {
captchaState = CaptchaState.Started
text = ""
},
showProgress = captchaState == CaptchaState.Started
)
}
}
}

@Composable
private fun CaptchaControlUI(
hideDialog: Boolean,
onHideDialogChanged: (Boolean) -> Unit,
userJourney: Boolean,
onUserJourneyChanged: (Boolean) -> Unit,
text: String,
onVerifyClick: () -> Unit,
showProgress: Boolean
Expand Down Expand Up @@ -114,6 +123,14 @@ class ComposeActivity : ComponentActivity() {
Text(text = "Hide Dialog (Passive Site Key)")
}

Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = userJourney,
onCheckedChange = onUserJourneyChanged
)
Text(text = "User Journey")
}

Button(
onClick = onVerifyClick,
modifier = Modifier
Expand Down
4 changes: 4 additions & 0 deletions sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ dependencies {
//noinspection GradleDependency
implementation 'androidx.appcompat:appcompat:1.3.1'
//noinspection GradleDependency
//implementation 'androidx.core:core:1.13.1'
//noinspection GradleDependency
//implementation 'androidx.recyclerview:recyclerview:1.4.0'
//noinspection GradleDependency
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.5' // max version https://github.com/FasterXML/jackson-databind/issues/3657
compileOnly 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
Expand Down
33 changes: 32 additions & 1 deletion sdk/src/main/java/com/hcaptcha/sdk/HCaptcha.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;

import com.hcaptcha.sdk.journeylitics.InMemorySink;
import com.hcaptcha.sdk.journeylitics.JLConfig;
import com.hcaptcha.sdk.journeylitics.JLEvent;
import com.hcaptcha.sdk.journeylitics.Journeylitics;
import com.hcaptcha.sdk.tasks.Task;

import java.util.List;

public final class HCaptcha extends Task<HCaptchaTokenResponse> implements IHCaptcha {
public static final String META_SITE_KEY = "com.hcaptcha.sdk.site-key";

Expand All @@ -26,6 +32,9 @@ public final class HCaptcha extends Task<HCaptchaTokenResponse> implements IHCap
@NonNull
private final HCaptchaInternalConfig internalConfig;

@Nullable
private InMemorySink journeySink;

private HCaptcha(@NonNull final Activity activity, @NonNull final HCaptchaInternalConfig internalConfig) {
this.activity = activity;
this.internalConfig = internalConfig;
Expand Down Expand Up @@ -96,6 +105,13 @@ void onFailure(final HCaptchaException exception) {
}
};
try {
// Initialize user journey tracking if enabled
if (Boolean.TRUE.equals(inputConfig.getUserJourney()) && journeySink == null) {
journeySink = new InMemorySink();
final JLConfig jlConfig = new JLConfig(journeySink);
Journeylitics.start(activity, jlConfig);
}

if (Boolean.TRUE.equals(inputConfig.getHideDialog())) {
// Overwrite certain config values in case the dialog is hidden to avoid behavior collision
this.config = inputConfig.toBuilder()
Expand Down Expand Up @@ -196,7 +212,22 @@ private HCaptcha startVerification(@Nullable final HCaptchaVerifyParams verifyPa
if (captchaVerifier == null) {
setException(new HCaptchaException(HCaptchaError.ERROR));
} else {
captchaVerifier.startVerification(activity, verifyParams);
HCaptchaVerifyParams finalParams = verifyParams;
if (journeySink != null && config != null && Boolean.TRUE.equals(config.getUserJourney())) {
final List<JLEvent> events = journeySink.getAndClearEvents();
if (!events.isEmpty()) {
if (finalParams == null) {
finalParams = HCaptchaVerifyParams.builder()
.userJourney(events)
.build();
} else {
finalParams = finalParams.toBuilder()
.userJourney(events)
.build();
}
}
}
captchaVerifier.startVerification(activity, finalParams);
}
return this;
}
Expand Down
6 changes: 6 additions & 0 deletions sdk/src/main/java/com/hcaptcha/sdk/HCaptchaConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,12 @@ public class HCaptchaConfig implements Serializable {
@NonNull
private Boolean disableHardwareAcceleration = true;

/**
* Enable / Disable user journey analytics tracking.
*/
@Builder.Default
private Boolean userJourney = false;

/**
* @deprecated use {@link #getJsSrc()} getter instead
*/
Expand Down
7 changes: 7 additions & 0 deletions sdk/src/main/java/com/hcaptcha/sdk/HCaptchaVerifyParams.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,11 @@ public class HCaptchaVerifyParams implements Serializable {
*/
@JsonProperty("rqdata")
private String rqdata;

/**
* Optional user journey events to be passed to hCaptcha.
* Contains user interaction events for analytics.
*/
@JsonProperty("userjourney")
private Object userJourney;
}
26 changes: 26 additions & 0 deletions sdk/src/main/java/com/hcaptcha/sdk/journeylitics/EventKind.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.hcaptcha.sdk.journeylitics;

import com.fasterxml.jackson.annotation.JsonValue;

/**
* Event kinds observed by the library
*/
enum EventKind {
screen("screen"),
click("click"),
drag("drag"),
gesture("gesture"),
edit("edit");

private final String value;

EventKind(String value) {
this.value = value;
}

@JsonValue
String getValue() {
return value;
}
}

43 changes: 43 additions & 0 deletions sdk/src/main/java/com/hcaptcha/sdk/journeylitics/FieldKey.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.hcaptcha.sdk.journeylitics;

/**
* Serialization enum for consistent field mapping across platforms
* Maps readable field names to short JSON keys for minification
*/
enum FieldKey {
// Top-level fields (always present)
KIND("k"),
VIEW("v"),
TIMESTAMP("ts"),
META("m"),

// Meta fields (nested under meta object)
ID("id"),
SCREEN("sc"),
ACTION("ac"),
VALUE("val"),
X("x"),
Y("y"),
INDEX("idx"),
SECTION("sct"),
ITEM("it"),
TARGET("tt"),
CONTROL("ct"),
GESTURE("gt"),
STATE("gs"),
TAPS("tap"),
CONTAINER_VIEW("cv"),
LENGTH("ln"),
COMPOSE("comp");

private final String jsonKey;

FieldKey(String jsonKey) {
this.jsonKey = jsonKey;
}

String getJsonKey() {
return jsonKey;
}
}

Loading
Loading