From 802cfe215f4cf554be3850cf4f0b98972db52afa Mon Sep 17 00:00:00 2001 From: Aliaksandr Babrykovich Date: Sun, 22 Mar 2026 20:13:26 +0100 Subject: [PATCH 1/3] chore: allow change siteKey in runtime --- .../com/hcaptcha/example/MainActivity.java | 38 ++++++++++++- .../src/main/res/layout/activity_main.xml | 56 +++++++++++++++++++ example-app/src/main/res/values/strings.xml | 5 ++ .../example/compose/ComposeActivity.kt | 52 ++++++++++++++++- 4 files changed, 148 insertions(+), 3 deletions(-) diff --git a/example-app/src/main/java/com/hcaptcha/example/MainActivity.java b/example-app/src/main/java/com/hcaptcha/example/MainActivity.java index 22ca750e..a0da5ac7 100644 --- a/example-app/src/main/java/com/hcaptcha/example/MainActivity.java +++ b/example-app/src/main/java/com/hcaptcha/example/MainActivity.java @@ -45,7 +45,8 @@ public class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getSimpleName(); - private static final String SITEKEY = "10000000-ffff-ffff-ffff-000000000001"; + private static final String SITEKEY_VISUAL = "00000000-0000-0000-0000-000000000000"; + private static final String SITEKEY_PASSIVE = "10000000-ffff-ffff-ffff-000000000001"; private static final int MAX_AUDIT_LOG_LINES = 100; private static final int TAB_CONFIGURATION = 0; private static final int TAB_CUSTOM = 1; @@ -54,6 +55,9 @@ public class MainActivity extends AppCompatActivity { private RadioGroup modeGroup; private RadioGroup sizeGroup; + private RadioGroup sitekeyGroup; + private TextInputLayout sitekeyInputLayout; + private TextInputEditText sitekeyInput; private CheckBox loading; private CheckBox disableHardwareAccel; private CheckBox themeDark; @@ -99,6 +103,9 @@ protected void onCreate(final Bundle savedInstanceState) { modeGroup = findViewById(R.id.challenge_mode_group); sizeGroup = findViewById(R.id.challenge_size_group); + sitekeyGroup = findViewById(R.id.sitekey_group); + sitekeyInputLayout = findViewById(R.id.sitekeyInputLayout); + sitekeyInput = findViewById(R.id.sitekeyInput); loading = findViewById(R.id.loading); disableHardwareAccel = findViewById(R.id.hwAccel); themeDark = findViewById(R.id.themeDark); @@ -140,6 +147,11 @@ protected void onCreate(final Bundle savedInstanceState) { updateCustomHostUiState(); }); + sitekeyGroup.setOnCheckedChangeListener((group, checkedId) -> { + sitekeyInputLayout.setVisibility(checkedId == R.id.sitekey_custom ? View.VISIBLE : View.GONE); + addAuditLog("Sitekey switched to " + sitekeyLabel(checkedId)); + }); + setPhoneModeUi(phoneModeSwitch.isChecked()); phoneModeSwitch.setOnCheckedChangeListener((button, checked) -> { setPhoneModeUi(checked); @@ -316,6 +328,28 @@ private HCaptchaSize getSelectedSize() { return HCaptchaSize.NORMAL; } + private String getSelectedSitekey() { + final int checkedId = sitekeyGroup.getCheckedRadioButtonId(); + if (checkedId == R.id.sitekey_passive) { + return SITEKEY_PASSIVE; + } + if (checkedId == R.id.sitekey_custom) { + final String custom = sitekeyInput.getText() != null ? sitekeyInput.getText().toString().trim() : ""; + return custom.isEmpty() ? SITEKEY_VISUAL : custom; + } + return SITEKEY_VISUAL; + } + + private String sitekeyLabel(final int checkedId) { + if (checkedId == R.id.sitekey_passive) { + return "Passive"; + } + if (checkedId == R.id.sitekey_custom) { + return "Custom"; + } + return "Challenge"; + } + private HCaptcha getConfiguredClient() { final HCaptcha client = HCaptcha.getClient(this); client.setEmbeddedContainer(selectedMode == HCaptchaRenderMode.EMBEDDED ? embeddedChallengeContainer : null); @@ -328,7 +362,7 @@ private HCaptchaConfig getConfig() { : getSelectedSize(); final boolean isDark = themeDark.isChecked(); return HCaptchaConfig.builder() - .siteKey(SITEKEY) + .siteKey(getSelectedSitekey()) .size(size) .renderMode(selectedMode) .loading(selectedMode != HCaptchaRenderMode.EMBEDDED && loading.isChecked()) diff --git a/example-app/src/main/res/layout/activity_main.xml b/example-app/src/main/res/layout/activity_main.xml index bbae90c6..46743a01 100644 --- a/example-app/src/main/res/layout/activity_main.xml +++ b/example-app/src/main/res/layout/activity_main.xml @@ -53,6 +53,62 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" + android:text="@string/sitekey" + android:textColor="@color/hc_text_primary" /> + + + + + + + + + + + + + + + + diff --git a/example-app/src/main/res/values/strings.xml b/example-app/src/main/res/values/strings.xml index 6a72bd56..d4c987d7 100644 --- a/example-app/src/main/res/values/strings.xml +++ b/example-app/src/main/res/values/strings.xml @@ -30,6 +30,11 @@ Phone input Country prefix (e.g. 44) Full phone number (e.g. +44123456789) + Sitekey + Challenge + Passive + Custom + xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx Request data (rqdata) Optional rqdata value Custom render host showcase 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 index 5036482e..a04f46b1 100644 --- 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 @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bolt @@ -36,6 +37,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Tab @@ -55,6 +57,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -73,6 +76,12 @@ import java.util.Locale class ComposeActivity : ComponentActivity() { private enum class ResultState { Idle, Verifying, Success, Error, Setup, Reset, Destroyed } private enum class TopTab { Configuration, CustomHost, Result, AuditLog } + private enum class SitekeyOption { Visual, Passive, Custom } + + companion object { + private const val SITEKEY_VISUAL = "00000000-0000-0000-0000-000000000000" + private const val SITEKEY_PASSIVE = "10000000-ffff-ffff-ffff-000000000001" + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -102,6 +111,8 @@ class ComposeActivity : ComponentActivity() { var selectedTab by remember { mutableStateOf(TopTab.Configuration) } var selectedMode by remember { mutableStateOf(HCaptchaRenderMode.DIALOG) } var selectedSize by remember { mutableStateOf(HCaptchaSize.NORMAL) } + var selectedSitekeyOption by remember { mutableStateOf(SitekeyOption.Passive) } + var customSitekey by remember { mutableStateOf("") } var loadingEnabled by remember { mutableStateOf(true) } var webDebugEnabled by remember { mutableStateOf(false) } @@ -150,6 +161,11 @@ class ComposeActivity : ComponentActivity() { val loadingForConfig = if (selectedMode == HCaptchaRenderMode.EMBEDDED) false else loadingEnabled val sizeForConfig = if (selectedMode == HCaptchaRenderMode.HEADLESS) HCaptchaSize.INVISIBLE else selectedSize + val activeSitekey = when (selectedSitekeyOption) { + SitekeyOption.Visual -> SITEKEY_VISUAL + SitekeyOption.Passive -> SITEKEY_PASSIVE + SitekeyOption.Custom -> customSitekey.ifBlank { SITEKEY_VISUAL } + } val config = remember( selectedMode, @@ -157,10 +173,11 @@ class ComposeActivity : ComponentActivity() { loadingForConfig, disableHwAccel, darkTheme, + activeSitekey, captchaRenderKey ) { HCaptchaConfig.builder() - .siteKey("10000000-ffff-ffff-ffff-000000000001") + .siteKey(activeSitekey) .size(sizeForConfig) .renderMode(selectedMode) .loading(loadingForConfig) @@ -310,6 +327,39 @@ class ComposeActivity : ComponentActivity() { verticalArrangement = Arrangement.spacedBy(10.dp) ) { Text("Configuration", style = MaterialTheme.typography.titleMedium, color = hcTextPrimary) + Text("Sitekey", color = hcTextPrimary) + InlineRadioRow( + labels = listOf("Passive", "Challenge", "Custom"), + selectedIndex = when (selectedSitekeyOption) { + SitekeyOption.Passive -> 0 + SitekeyOption.Visual -> 1 + SitekeyOption.Custom -> 2 + }, + onSelected = { + selectedSitekeyOption = when (it) { + 1 -> SitekeyOption.Visual + 2 -> SitekeyOption.Custom + else -> SitekeyOption.Passive + } + val label = when (selectedSitekeyOption) { + SitekeyOption.Visual -> "Challenge" + SitekeyOption.Passive -> "Passive" + SitekeyOption.Custom -> "Custom" + } + addAudit("Sitekey switched to $label") + } + ) + if (selectedSitekeyOption == SitekeyOption.Custom) { + OutlinedTextField( + value = customSitekey, + onValueChange = { customSitekey = it }, + label = { Text("Custom sitekey") }, + placeholder = { Text("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii) + ) + } Text("Render mode", color = hcTextPrimary) InlineRadioRow( labels = listOf("Dialog", "Headless", "Embedded"), From 78b8850123e38c23c27314957df53a22f48e3b2f Mon Sep 17 00:00:00 2001 From: Aliaksandr Babrykovich Date: Sun, 22 Mar 2026 21:46:54 +0100 Subject: [PATCH 2/3] chore: remove no-op Setup button and setupArmed state --- .../example/compose/ComposeActivity.kt | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) 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 index a04f46b1..5b26aa64 100644 --- 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 @@ -21,7 +21,6 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bolt -import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Refresh @@ -74,7 +73,7 @@ import java.util.Date import java.util.Locale class ComposeActivity : ComponentActivity() { - private enum class ResultState { Idle, Verifying, Success, Error, Setup, Reset, Destroyed } + private enum class ResultState { Idle, Verifying, Success, Error, Reset, Destroyed } private enum class TopTab { Configuration, CustomHost, Result, AuditLog } private enum class SitekeyOption { Visual, Passive, Custom } @@ -125,7 +124,6 @@ class ComposeActivity : ComponentActivity() { val auditLog = remember { mutableStateListOf() } var captchaVisible by remember { mutableStateOf(false) } - var setupArmed by remember { mutableStateOf(false) } var captchaRenderKey by remember { mutableIntStateOf(0) } var customWaitingForOpen by remember { mutableStateOf(false) } var customStatusMessage by remember { mutableStateOf(null) } @@ -153,7 +151,6 @@ class ComposeActivity : ComponentActivity() { fun resetCaptcha() { captchaVisible = false captchaRenderKey += 1 - setupArmed = false customWaitingForOpen = false customStatusMessage = null customStatusIsError = false @@ -218,18 +215,6 @@ class ComposeActivity : ComponentActivity() { label = { Text("Reset") }, colors = navItemColors ) - NavigationBarItem( - selected = false, - onClick = { - setupArmed = true - resultState = ResultState.Setup - resultMessage = "setup armed" - addAudit("Setup armed") - }, - icon = { Icon(Icons.Default.Build, contentDescription = null) }, - label = { Text("Setup") }, - colors = navItemColors - ) NavigationBarItem( selected = false, onClick = { @@ -243,7 +228,7 @@ class ComposeActivity : ComponentActivity() { customStatusMessage = null customStatusIsError = false } - if (!setupArmed) addAudit("Verify without setup") else addAudit("Verify") + addAudit("Verify") }, icon = { Icon(Icons.Default.CheckCircle, contentDescription = null) }, label = { Text("Verify") }, @@ -429,7 +414,6 @@ class ComposeActivity : ComponentActivity() { HCaptchaCompose(config = config) { response -> when (response) { is HCaptchaResponse.Success -> { - setupArmed = false captchaVisible = false customWaitingForOpen = false customStatusMessage = "Success. See Result tab for details." @@ -441,7 +425,6 @@ class ComposeActivity : ComponentActivity() { } is HCaptchaResponse.Failure -> { - setupArmed = false captchaVisible = false customWaitingForOpen = false customStatusMessage = "Error. See Result tab for details." @@ -584,7 +567,6 @@ class ComposeActivity : ComponentActivity() { HCaptchaCompose(config = config) { response -> when (response) { is HCaptchaResponse.Success -> { - setupArmed = false captchaVisible = false resultState = ResultState.Success lastToken = response.token @@ -593,7 +575,6 @@ class ComposeActivity : ComponentActivity() { } is HCaptchaResponse.Failure -> { - setupArmed = false captchaVisible = false resultState = ResultState.Error lastToken = null From 182ec8ddb061fb71cc33b21303903a7ed1d7324c Mon Sep 17 00:00:00 2001 From: Aliaksandr Babrykovich Date: Sun, 22 Mar 2026 23:36:06 +0100 Subject: [PATCH 3/3] fix(sdk): prevent stale readyForInteraction after dialog dismiss --- .../java/com/hcaptcha/sdk/HCaptchaDialogFragment.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java index cfe91c0c..130b1c6a 100644 --- a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java +++ b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java @@ -196,6 +196,7 @@ public View onCreateView(@Nullable LayoutInflater inflater, public void onDestroy() { HCaptchaLog.d("DialogFragment.onDestroy"); super.onDestroy(); + readyForInteraction = false; if (webViewHelper != null) { webViewHelper.reset(); } @@ -210,12 +211,15 @@ public void onStart() { final Window window = dialog.getWindow(); window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); defaultDimAmount = window.getAttributes().dimAmount; - if (Boolean.FALSE.equals(webViewHelper.getConfig().getLoading())) { - // Remove dialog shadow to appear completely invisible + if (!readyForInteraction && Boolean.FALSE.equals(webViewHelper.getConfig().getLoading())) { window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); window.setDimAmount(0); } } + if (!readyForInteraction && webViewHelper != null) { + HCaptchaLog.d("DialogFragment.onStart: re-triggering onLoaded after reset"); + onLoaded(); + } } @Override