From 8c2306ea667bfeda159d094556622ec5b4b18f24 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 15 Jan 2025 09:15:58 -0500 Subject: [PATCH 01/15] Preliminary HCaptcha support. --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 3 + .../org/wikipedia/settings/AboutActivity.kt | 66 +++++++++++++++++++ gradle/libs.versions.toml | 2 + settings.gradle.kts | 1 + 5 files changed, 73 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index 83debfb15a5..a1641d7f315 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -195,6 +195,7 @@ dependencies { implementation libs.drawerlayout implementation libs.swiperefreshlayout implementation libs.work.runtime.ktx + implementation libs.hcaptcha implementation libs.metrics.platform implementation libs.glide diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 70981b00f97..0821e6ab6eb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -86,6 +86,9 @@ + + + ().forEach { it.movementMethod = LinkMovementMethodCompat.getInstance() } + + + binding.aboutVersionText.setOnClickListener { + verifyHCaptcha() + //hCaptcha = HCaptcha.getClient(this).setup(getHCaptchaConfig()) + //setupHCaptchaClient(hCaptcha) + } } + + private fun setupHCaptchaClient(captcha: HCaptcha?) { + captcha?.addOnSuccessListener { response -> + tokenResponse = response + val userResponseToken = response.tokenResult + L.d("hCaptcha token: $userResponseToken") + }?.addOnFailureListener { e -> + L.e("hCaptcha failed: ${e.message} (${e.statusCode})") + tokenResponse = null + }?.addOnOpenListener { + FeedbackUtil.showMessage(this, "hCaptcha shown") + } + } + + private fun getHCaptchaConfig(): HCaptchaConfig { + val size = HCaptchaSize.NORMAL + return HCaptchaConfig.builder() + .siteKey("10000000-ffff-ffff-ffff-000000000001") // << TODO: use our site key + .size(size) + .loading(true) + .hideDialog(false) + .tokenExpiration(10) + .diagnosticLog(true) + .retryPredicate { config, exception -> + exception.hCaptchaError == HCaptchaError.SESSION_TIMEOUT + } + .build() + } + + private fun verifyHCaptcha() { + if (hCaptcha != null) { + hCaptcha?.verifyWithHCaptcha() + } else { + hCaptcha = HCaptcha.getClient(this).verifyWithHCaptcha(getHCaptchaConfig()) + setupHCaptchaClient(hCaptcha) + } + } + + private fun resetHCaptcha() { + hCaptcha?.reset() + hCaptcha = null + } + + private fun markHCaptchaUsed() { + tokenResponse?.markUsed() + } + + + private class AboutLogoClickListener : View.OnClickListener { private var secretClickCount = 0 override fun onClick(v: View) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 91c795391a1..d1d7f909244 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ googlePayVersion = "19.4.0" googleServices = "4.4.2" gradle = "8.8.0" hamcrest = "3.0" +hcaptcha = "4.0.3" installreferrer = "2.2" jsoup = "1.18.3" junit = "4.13.2" @@ -81,6 +82,7 @@ glide-ksp = { module = "com.github.bumptech.glide:ksp", version.ref = "glideVers google-services = { module = "com.google.gms:google-services", version.ref = "googleServices" } gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" } +hcaptcha = { module = "com.github.hCaptcha.hcaptcha-android-sdk:sdk", version.ref = "hcaptcha" } installreferrer = { module = "com.android.installreferrer:installreferrer", version.ref = "installreferrer" } jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } junit = { module = "junit:junit", version.ref = "junit" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 1a6a4962cae..78d1cb4c4e5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,6 +11,7 @@ dependencyResolutionManagement { google() mavenCentral() mavenLocal() + maven { setUrl("https://jitpack.io") } } } From b3d33dae096bc159433dc4f3bc38739005b1dcbd Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Fri, 19 Sep 2025 12:32:25 -0400 Subject: [PATCH 02/15] Further. --- app/src/main/AndroidManifest.xml | 3 - .../createaccount/CreateAccountActivity.kt | 69 +++++++++++++++++- .../org/wikipedia/settings/AboutActivity.kt | 70 +------------------ gradle/libs.versions.toml | 2 +- 4 files changed, 70 insertions(+), 74 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f3611a6219a..458d085d352 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -86,9 +86,6 @@ - - - + // exception.hCaptchaError == HCaptchaError.SESSION_TIMEOUT + //} + .build()) + + hCaptcha?.addOnSuccessListener { response -> + tokenResponse = response + val userResponseToken = response.tokenResult + L.d("hCaptcha token: $userResponseToken") + finish() + }?.addOnFailureListener { e -> + L.e("hCaptcha failed: ${e.message} (${e.statusCode})") + tokenResponse = null + FeedbackUtil.showMessage(this, "hCaptcha failed: ${e.message} (${e.statusCode})") + }?.addOnOpenListener { + FeedbackUtil.showMessage(this, "hCaptcha shown") + } + } + hCaptcha?.verifyWithHCaptcha() + } + + private fun resetHCaptcha() { + hCaptcha?.reset() + hCaptcha = null + } + private fun handleAccountCreationError(message: String) { if (message.contains("blocked")) { FeedbackUtil.makeSnackbar(this, getString(R.string.create_account_ip_block_message)) @@ -210,6 +276,7 @@ class CreateAccountActivity : BaseActivity() { } public override fun onDestroy() { + resetHCaptcha() captchaHandler.dispose() userNameTextWatcher?.let { binding.createAccountUsername.editText?.removeTextChangedListener(it) } super.onDestroy() diff --git a/app/src/main/java/org/wikipedia/settings/AboutActivity.kt b/app/src/main/java/org/wikipedia/settings/AboutActivity.kt index a88a653b7ec..100c8add1b6 100644 --- a/app/src/main/java/org/wikipedia/settings/AboutActivity.kt +++ b/app/src/main/java/org/wikipedia/settings/AboutActivity.kt @@ -37,11 +37,6 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.hcaptcha.sdk.HCaptcha -import com.hcaptcha.sdk.HCaptchaConfig -import com.hcaptcha.sdk.HCaptchaError -import com.hcaptcha.sdk.HCaptchaSize -import com.hcaptcha.sdk.HCaptchaTokenResponse import kotlinx.coroutines.launch import org.wikipedia.BuildConfig import org.wikipedia.R @@ -55,14 +50,8 @@ import org.wikipedia.compose.theme.BaseTheme import org.wikipedia.compose.theme.WikipediaTheme import org.wikipedia.theme.Theme import org.wikipedia.util.DeviceUtil -import org.wikipedia.util.FeedbackUtil -import org.wikipedia.util.log.L class AboutActivity : BaseActivity() { - private var hCaptcha: HCaptcha? = null - private var tokenResponse: HCaptchaTokenResponse? = null - - private val credits = listOf( LinkTextData( text = "Balloon", @@ -118,70 +107,13 @@ class AboutActivity : BaseActivity() { versionName = BuildConfig.VERSION_NAME, credits = credits, onBackButtonClick = { - //onBackPressedDispatcher.onBackPressed() - - - - verifyHCaptcha() - //hCaptcha = HCaptcha.getClient(this).setup(getHCaptchaConfig()) - //setupHCaptchaClient(hCaptcha) - - - + onBackPressedDispatcher.onBackPressed() } ) } } } - private fun setupHCaptchaClient(captcha: HCaptcha?) { - captcha?.addOnSuccessListener { response -> - tokenResponse = response - val userResponseToken = response.tokenResult - L.d("hCaptcha token: $userResponseToken") - finish() - }?.addOnFailureListener { e -> - L.e("hCaptcha failed: ${e.message} (${e.statusCode})") - tokenResponse = null - FeedbackUtil.showMessage(this, "hCaptcha failed: ${e.message} (${e.statusCode})") - }?.addOnOpenListener { - FeedbackUtil.showMessage(this, "hCaptcha shown") - } - } - - private fun getHCaptchaConfig(): HCaptchaConfig { - val size = HCaptchaSize.NORMAL - return HCaptchaConfig.builder() - .siteKey("f1f21d64-6384-4114-b7d0-d9d23e203b4a") // << TODO: use our site key - .size(size) - .loading(true) - .hideDialog(false) - .tokenExpiration(10) - .diagnosticLog(true) - .retryPredicate { config, exception -> - exception.hCaptchaError == HCaptchaError.SESSION_TIMEOUT - } - .build() - } - - private fun verifyHCaptcha() { - if (hCaptcha != null) { - hCaptcha?.verifyWithHCaptcha() - } else { - hCaptcha = HCaptcha.getClient(this).verifyWithHCaptcha(getHCaptchaConfig()) - setupHCaptchaClient(hCaptcha) - } - } - - private fun resetHCaptcha() { - hCaptcha?.reset() - hCaptcha = null - } - - private fun markHCaptchaUsed() { - tokenResponse?.markUsed() - } - companion object { const val SECRET_CLICK_LIMIT = 7 } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 51ee4dcac2d..e5e695b9daa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ googlePayVersion = "19.4.0" googleServices = "4.4.3" gradle = "8.13.0" hamcrest = "3.0" -hcaptcha = "4.0.3" +hcaptcha = "4.2.3" installreferrer = "2.2" jsoup = "1.21.2" junit = "4.13.2" From ad71a6d0965e620d35daeb271b5b6a686e83bae8 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Fri, 19 Sep 2025 13:51:12 -0400 Subject: [PATCH 03/15] Yep. --- .../org/wikipedia/createaccount/CreateAccountActivity.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt index 4ceae8b84b4..99c781a25f9 100644 --- a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt +++ b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt @@ -203,11 +203,15 @@ class CreateAccountActivity : BaseActivity() { hCaptcha?.setup( HCaptchaConfig.builder() .siteKey("45205f58-be1c-40f0-b286-07a4498ea3da") + //.siteKey("f1f21d64-6384-4114-b7d0-d9d23e203b4a") .theme(if (WikipediaApp.instance.currentTheme.isDark) HCaptchaTheme.DARK else HCaptchaTheme.LIGHT) .size(HCaptchaSize.NORMAL) .host("meta.wikimedia.org") - .jsSrc("https://assets-hcaptcha.wikimedia.org/captcha/1/73f27c192b38c05ce2ebce596a0e28f88a2a56bf/secure-api.js") + + //.jsSrc("https://assets-hcaptcha.wikimedia.org/captcha/1/73f27c192b38c05ce2ebce596a0e28f88a2a56bf/secure-api.js") + .jsSrc("https://js.hcaptcha.com/1/api.js") + .endpoint("https://hcaptcha.wikimedia.org") .assethost("https://assets-hcaptcha.wikimedia.org") .imghost("https://imgs-hcaptcha.wikimedia.org") From 37272e6fc5b727b07c15ebe27928ee1bcdad8072 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Mon, 22 Sep 2025 10:15:51 -0400 Subject: [PATCH 04/15] Close the loop. --- .../createaccount/CreateAccountActivity.kt | 24 +++++++++---------- .../CreateAccountActivityViewModel.kt | 15 ++++++++---- .../dataclient/mwapi/MwAuthManagerInfo.kt | 2 +- .../dataclient/mwapi/MwQueryResult.kt | 4 ++++ 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt index 99c781a25f9..30738f83e9c 100644 --- a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt +++ b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt @@ -100,6 +100,9 @@ class CreateAccountActivity : BaseActivity() { is CreateAccountActivityViewModel.AccountInfoState.DoCreateAccount -> { doCreateAccount(it.token) } + is CreateAccountActivityViewModel.AccountInfoState.HandleHCaptcha -> { + showHCaptcha() + } is CreateAccountActivityViewModel.AccountInfoState.HandleCaptcha -> { captchaHandler.handleCaptcha(it.token, CaptchaResult(it.captchaId)) } @@ -202,23 +205,17 @@ class CreateAccountActivity : BaseActivity() { hCaptcha = HCaptcha.getClient(this) hCaptcha?.setup( HCaptchaConfig.builder() - .siteKey("45205f58-be1c-40f0-b286-07a4498ea3da") - //.siteKey("f1f21d64-6384-4114-b7d0-d9d23e203b4a") + .siteKey("f1f21d64-6384-4114-b7d0-d9d23e203b4a") .theme(if (WikipediaApp.instance.currentTheme.isDark) HCaptchaTheme.DARK else HCaptchaTheme.LIGHT) - .size(HCaptchaSize.NORMAL) - .host("meta.wikimedia.org") - //.jsSrc("https://assets-hcaptcha.wikimedia.org/captcha/1/73f27c192b38c05ce2ebce596a0e28f88a2a56bf/secure-api.js") - .jsSrc("https://js.hcaptcha.com/1/api.js") - + .jsSrc("https://assets-hcaptcha.wikimedia.org/1/api.js") .endpoint("https://hcaptcha.wikimedia.org") .assethost("https://assets-hcaptcha.wikimedia.org") .imghost("https://imgs-hcaptcha.wikimedia.org") .reportapi("https://report-hcaptcha.wikimedia.org") .sentry(false) - //.loading(true) //.locale("en") //.size(HCaptchaSize.INVISIBLE) @@ -228,13 +225,12 @@ class CreateAccountActivity : BaseActivity() { //.retryPredicate { config, exception -> // exception.hCaptchaError == HCaptchaError.SESSION_TIMEOUT //} + .build()) hCaptcha?.addOnSuccessListener { response -> tokenResponse = response - val userResponseToken = response.tokenResult - L.d("hCaptcha token: $userResponseToken") - finish() + doCreateAccount(viewModel.token.orEmpty(), hCaptchaToken = response.tokenResult) }?.addOnFailureListener { e -> L.e("hCaptcha failed: ${e.message} (${e.statusCode})") tokenResponse = null @@ -265,13 +261,15 @@ class CreateAccountActivity : BaseActivity() { L.w("Account creation failed with result $message") } - private fun doCreateAccount(token: String) { + private fun doCreateAccount(token: String, hCaptchaToken: String? = null) { showProgressBar(true) val email = getText(binding.createAccountEmail).ifEmpty { null } val password = getText(binding.createAccountPasswordInput) val repeat = getText(binding.createAccountPasswordRepeat) val userName = getText(binding.createAccountUsername) - viewModel.doCreateAccount(token, captchaHandler.captchaId().toString(), captchaHandler.captchaWord().toString(), userName, password, repeat, email) + viewModel.doCreateAccount(token, captchaHandler.captchaId(), + if (hCaptchaToken.isNullOrEmpty()) captchaHandler.captchaWord() else hCaptchaToken, + userName, password, repeat, email) } public override fun onStop() { diff --git a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivityViewModel.kt b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivityViewModel.kt index 9a98ed5b718..b6f63c2e0f0 100644 --- a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivityViewModel.kt +++ b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivityViewModel.kt @@ -26,6 +26,8 @@ class CreateAccountActivityViewModel : ViewModel() { private val _verifyUserNameState = MutableSharedFlow() val verifyUserNameState = _verifyUserNameState.asSharedFlow() + var token: String? = null + private var verifyUserNameJob: Job? = null fun createAccountInfo() { @@ -33,19 +35,21 @@ class CreateAccountActivityViewModel : ViewModel() { _createAccountInfoState.value = AccountInfoState.Error(throwable) }) { val response = ServiceFactory.get(WikipediaApp.instance.wikiSite).getAuthManagerInfo() - val token = response.query?.createAccountToken() + token = response.query?.createAccountToken() val captchaId = response.query?.captchaId() if (token.isNullOrEmpty()) { _createAccountInfoState.value = AccountInfoState.InvalidToken + } else if (response.query?.hasHCaptchaRequest() == true) { + _createAccountInfoState.value = AccountInfoState.HandleHCaptcha(token!!) } else if (!captchaId.isNullOrEmpty()) { - _createAccountInfoState.value = AccountInfoState.HandleCaptcha(token, captchaId) + _createAccountInfoState.value = AccountInfoState.HandleCaptcha(token!!, captchaId) } else { - _createAccountInfoState.value = AccountInfoState.DoCreateAccount(token) + _createAccountInfoState.value = AccountInfoState.DoCreateAccount(token!!) } } } - fun doCreateAccount(token: String, captchaId: String, captchaWord: String, userName: String, password: String, repeat: String, email: String?) { + fun doCreateAccount(token: String, captchaId: String?, captchaWord: String?, userName: String, password: String, repeat: String, email: String?) { viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> _doCreateAccountState.value = CreateAccountState.Error(throwable) }) { @@ -84,7 +88,8 @@ class CreateAccountActivityViewModel : ViewModel() { open class AccountInfoState { data class DoCreateAccount(val token: String) : AccountInfoState() - data class HandleCaptcha(val token: String?, val captchaId: String) : AccountInfoState() + data class HandleCaptcha(val token: String, val captchaId: String) : AccountInfoState() + data class HandleHCaptcha(val token: String) : AccountInfoState() data object InvalidToken : AccountInfoState() data class Error(val throwable: Throwable) : AccountInfoState() } diff --git a/app/src/main/java/org/wikipedia/dataclient/mwapi/MwAuthManagerInfo.kt b/app/src/main/java/org/wikipedia/dataclient/mwapi/MwAuthManagerInfo.kt index 24faf1ab0a0..d75599747ba 100644 --- a/app/src/main/java/org/wikipedia/dataclient/mwapi/MwAuthManagerInfo.kt +++ b/app/src/main/java/org/wikipedia/dataclient/mwapi/MwAuthManagerInfo.kt @@ -11,7 +11,7 @@ internal class MwAuthManagerInfo { internal class Request(val id: String? = null, private val metadata: Map? = null, private val required: String? = null, - private val provider: String? = null, + val provider: String? = null, private val account: String? = null, val fields: Map? = null) diff --git a/app/src/main/java/org/wikipedia/dataclient/mwapi/MwQueryResult.kt b/app/src/main/java/org/wikipedia/dataclient/mwapi/MwQueryResult.kt index 428134591f7..59e3791c245 100644 --- a/app/src/main/java/org/wikipedia/dataclient/mwapi/MwQueryResult.kt +++ b/app/src/main/java/org/wikipedia/dataclient/mwapi/MwQueryResult.kt @@ -79,6 +79,10 @@ class MwQueryResult { return amInfo?.requests?.find { it.fields?.containsKey(key) == true }?.fields?.get(key)?.value } + fun hasHCaptchaRequest(): Boolean { + return amInfo?.requests?.find { it.provider.orEmpty().lowercase().contains("hcaptcha") } != null + } + fun getUserResponse(userName: String): UserInfo? { // MediaWiki user names are case sensitive, but the first letter is always capitalized. return users?.find { StringUtil.capitalize(userName) == it.name } From 8f5c267a38b28e02b735adebb44ab3ad2aac2328 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Tue, 23 Sep 2025 07:42:38 -0400 Subject: [PATCH 05/15] Refactor into helper class. --- .../org/wikipedia/captcha/HCaptchaHelper.kt | 70 +++++++++++++++++++ .../createaccount/CreateAccountActivity.kt | 65 ++--------------- 2 files changed, 77 insertions(+), 58 deletions(-) create mode 100644 app/src/main/java/org/wikipedia/captcha/HCaptchaHelper.kt diff --git a/app/src/main/java/org/wikipedia/captcha/HCaptchaHelper.kt b/app/src/main/java/org/wikipedia/captcha/HCaptchaHelper.kt new file mode 100644 index 00000000000..6d9b814b0b9 --- /dev/null +++ b/app/src/main/java/org/wikipedia/captcha/HCaptchaHelper.kt @@ -0,0 +1,70 @@ +package org.wikipedia.captcha + +import androidx.fragment.app.FragmentActivity +import com.hcaptcha.sdk.HCaptcha +import com.hcaptcha.sdk.HCaptchaConfig +import com.hcaptcha.sdk.HCaptchaTheme +import com.hcaptcha.sdk.HCaptchaTokenResponse +import org.wikipedia.WikipediaApp +import org.wikipedia.util.FeedbackUtil +import org.wikipedia.util.log.L + +class HCaptchaHelper( + private val activity: FragmentActivity, + private val callback: Callback +) { + fun interface Callback { + fun onSuccess(token: String) + } + + private var hCaptcha: HCaptcha? = null + private var tokenResponse: HCaptchaTokenResponse? = null + + + fun show() { + if (hCaptcha == null) { + hCaptcha = HCaptcha.getClient(activity) + hCaptcha?.setup( + HCaptchaConfig.builder() + .siteKey("f1f21d64-6384-4114-b7d0-d9d23e203b4a") + .theme(if (WikipediaApp.instance.currentTheme.isDark) HCaptchaTheme.DARK else HCaptchaTheme.LIGHT) + .host("meta.wikimedia.org") + + .jsSrc("https://assets-hcaptcha.wikimedia.org/1/api.js") + .endpoint("https://hcaptcha.wikimedia.org") + .assethost("https://assets-hcaptcha.wikimedia.org") + .imghost("https://imgs-hcaptcha.wikimedia.org") + .reportapi("https://report-hcaptcha.wikimedia.org") + .sentry(false) + + //.loading(true) + //.locale("en") + //.size(HCaptchaSize.INVISIBLE) + //.hideDialog(false) + //.tokenExpiration(10) + //.diagnosticLog(true) + //.retryPredicate { config, exception -> + // exception.hCaptchaError == HCaptchaError.SESSION_TIMEOUT + //} + + .build()) + + hCaptcha?.addOnSuccessListener { response -> + tokenResponse = response + callback.onSuccess(response.tokenResult) + }?.addOnFailureListener { e -> + tokenResponse = null + L.e("hCaptcha failed: ${e.message} (${e.statusCode})") + FeedbackUtil.showMessage(activity, "${e.message} (${e.statusCode})") + }?.addOnOpenListener { + L.d("hCaptcha opened") + } + } + hCaptcha?.verifyWithHCaptcha() + } + + fun cleanup() { + hCaptcha?.reset() + hCaptcha = null + } +} \ No newline at end of file diff --git a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt index 30738f83e9c..daf033a5e36 100644 --- a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt +++ b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt @@ -19,11 +19,6 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.TextInputLayout -import com.hcaptcha.sdk.HCaptcha -import com.hcaptcha.sdk.HCaptchaConfig -import com.hcaptcha.sdk.HCaptchaSize -import com.hcaptcha.sdk.HCaptchaTheme -import com.hcaptcha.sdk.HCaptchaTokenResponse import kotlinx.coroutines.launch import org.wikipedia.R import org.wikipedia.WikipediaApp @@ -32,6 +27,7 @@ import org.wikipedia.analytics.eventplatform.CreateAccountEvent import org.wikipedia.auth.AccountUtil import org.wikipedia.captcha.CaptchaHandler import org.wikipedia.captcha.CaptchaResult +import org.wikipedia.captcha.HCaptchaHelper import org.wikipedia.databinding.ActivityCreateAccountBinding import org.wikipedia.page.PageTitle import org.wikipedia.util.DeviceUtil @@ -54,8 +50,9 @@ class CreateAccountActivity : BaseActivity() { private var userNameTextWatcher: TextWatcher? = null private val viewModel: CreateAccountActivityViewModel by viewModels() - private var hCaptcha: HCaptcha? = null - private var tokenResponse: HCaptchaTokenResponse? = null + private val hCaptchaHelper = HCaptchaHelper(this) { token -> + doCreateAccount(viewModel.token.orEmpty(), hCaptchaToken = token) + } public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -101,7 +98,7 @@ class CreateAccountActivity : BaseActivity() { doCreateAccount(it.token) } is CreateAccountActivityViewModel.AccountInfoState.HandleHCaptcha -> { - showHCaptcha() + hCaptchaHelper.show() } is CreateAccountActivityViewModel.AccountInfoState.HandleCaptcha -> { captchaHandler.handleCaptcha(it.token, CaptchaResult(it.captchaId)) @@ -177,7 +174,7 @@ class CreateAccountActivity : BaseActivity() { - showHCaptcha() + hCaptchaHelper.show() @@ -199,54 +196,6 @@ class CreateAccountActivity : BaseActivity() { } } - private fun showHCaptcha() { - if (hCaptcha == null) { - - hCaptcha = HCaptcha.getClient(this) - hCaptcha?.setup( - HCaptchaConfig.builder() - .siteKey("f1f21d64-6384-4114-b7d0-d9d23e203b4a") - .theme(if (WikipediaApp.instance.currentTheme.isDark) HCaptchaTheme.DARK else HCaptchaTheme.LIGHT) - .host("meta.wikimedia.org") - - .jsSrc("https://assets-hcaptcha.wikimedia.org/1/api.js") - .endpoint("https://hcaptcha.wikimedia.org") - .assethost("https://assets-hcaptcha.wikimedia.org") - .imghost("https://imgs-hcaptcha.wikimedia.org") - .reportapi("https://report-hcaptcha.wikimedia.org") - .sentry(false) - - //.loading(true) - //.locale("en") - //.size(HCaptchaSize.INVISIBLE) - //.hideDialog(false) - //.tokenExpiration(10) - //.diagnosticLog(true) - //.retryPredicate { config, exception -> - // exception.hCaptchaError == HCaptchaError.SESSION_TIMEOUT - //} - - .build()) - - hCaptcha?.addOnSuccessListener { response -> - tokenResponse = response - doCreateAccount(viewModel.token.orEmpty(), hCaptchaToken = response.tokenResult) - }?.addOnFailureListener { e -> - L.e("hCaptcha failed: ${e.message} (${e.statusCode})") - tokenResponse = null - FeedbackUtil.showMessage(this, "hCaptcha failed: ${e.message} (${e.statusCode})") - }?.addOnOpenListener { - FeedbackUtil.showMessage(this, "hCaptcha shown") - } - } - hCaptcha?.verifyWithHCaptcha() - } - - private fun resetHCaptcha() { - hCaptcha?.reset() - hCaptcha = null - } - private fun handleAccountCreationError(message: String) { if (message.contains("blocked")) { FeedbackUtil.makeSnackbar(this, getString(R.string.create_account_ip_block_message)) @@ -278,7 +227,7 @@ class CreateAccountActivity : BaseActivity() { } public override fun onDestroy() { - resetHCaptcha() + hCaptchaHelper.cleanup() captchaHandler.dispose() userNameTextWatcher?.let { binding.createAccountUsername.editText?.removeTextChangedListener(it) } super.onDestroy() From 30892470817f1ea311a61d11b7ac1d41145e6200 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 24 Sep 2025 09:02:30 -0400 Subject: [PATCH 06/15] Underpinnings for hCaptcha. --- .../java/org/wikipedia/settings/RemoteConfig.kt | 15 +++++++++++++++ .../wikipedia/settings/RemoteConfigRefreshTask.kt | 15 +++++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/wikipedia/settings/RemoteConfig.kt b/app/src/main/java/org/wikipedia/settings/RemoteConfig.kt index c8105043f49..a0eb1a78037 100644 --- a/app/src/main/java/org/wikipedia/settings/RemoteConfig.kt +++ b/app/src/main/java/org/wikipedia/settings/RemoteConfig.kt @@ -1,5 +1,6 @@ package org.wikipedia.settings +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import org.wikipedia.json.JsonUtil import org.wikipedia.util.log.L @@ -43,5 +44,19 @@ object RemoteConfig { @Serializable class RemoteConfigAndroidV1 { val disableReadingListSync = false + val hCaptcha: RemoteConfigHCaptcha? = null + } + + @Suppress("unused") + @Serializable + class RemoteConfigHCaptcha { + val baseURL = "" + val jsSrc = "" + val endpoint = "" + @SerialName("assethost") val assetHost = "" + @SerialName("imghost") val imgHost = "" + @SerialName("reportapi") val reportApi = "" + val sentry = false + val siteKey = "" } } diff --git a/app/src/main/java/org/wikipedia/settings/RemoteConfigRefreshTask.kt b/app/src/main/java/org/wikipedia/settings/RemoteConfigRefreshTask.kt index 593633023c1..705a3181acf 100644 --- a/app/src/main/java/org/wikipedia/settings/RemoteConfigRefreshTask.kt +++ b/app/src/main/java/org/wikipedia/settings/RemoteConfigRefreshTask.kt @@ -1,6 +1,7 @@ package org.wikipedia.settings import org.wikipedia.WikipediaApp +import org.wikipedia.auth.AccountUtil import org.wikipedia.dataclient.ServiceFactory import org.wikipedia.recurring.RecurringTask import java.util.Date @@ -10,17 +11,19 @@ class RemoteConfigRefreshTask() : RecurringTask() { override val name = "remote-config-refresher" override fun shouldRun(lastRun: Date): Boolean { - return millisSinceLastRun(lastRun) >= TimeUnit.DAYS.toMillis(1) + return millisSinceLastRun(lastRun) >= TimeUnit.HOURS.toMillis(6) } override suspend fun run(lastRun: Date) { val config = ServiceFactory.getRest(WikipediaApp.instance.wikiSite).getConfiguration() RemoteConfig.updateConfig(config) - val userInfo = ServiceFactory.get(WikipediaApp.instance.wikiSite).getUserInfo() - // This clumsy comparison is necessary because the field is an integer value when enabled, but an empty string when disabled. - // Since we want the default to lean towards opt-in, we check very specifically for an empty string, to make sure the user has opted out. - val fundraisingOptOut = userInfo.query?.userInfo?.options?.fundraisingOptIn?.toString()?.replace("\"", "")?.isEmpty() - Prefs.donationBannerOptIn = fundraisingOptOut != true + if (AccountUtil.isLoggedIn) { + val userInfo = ServiceFactory.get(WikipediaApp.instance.wikiSite).getUserInfo() + // This clumsy comparison is necessary because the field is an integer value when enabled, but an empty string when disabled. + // Since we want the default to lean towards opt-in, we check very specifically for an empty string, to make sure the user has opted out. + val fundraisingOptOut = userInfo.query?.userInfo?.options?.fundraisingOptIn?.toString()?.replace("\"", "")?.isEmpty() + Prefs.donationBannerOptIn = fundraisingOptOut != true + } } } From 93013816c0572138910f5f8afdf985433ddd2cf2 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 15 Jan 2025 09:15:58 -0500 Subject: [PATCH 07/15] Preliminary HCaptcha support. --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 3 +++ gradle/libs.versions.toml | 2 ++ settings.gradle.kts | 1 + 4 files changed, 7 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index 891074c7263..ab62cba4e2f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -197,6 +197,7 @@ dependencies { implementation libs.drawerlayout implementation libs.swiperefreshlayout implementation libs.work.runtime.ktx + implementation libs.hcaptcha implementation libs.metrics.platform implementation libs.okhttp.tls diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 458d085d352..d9b916ca990 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -86,6 +86,9 @@ + + + Date: Fri, 19 Sep 2025 12:32:25 -0400 Subject: [PATCH 08/15] Further. --- app/src/main/AndroidManifest.xml | 3 - .../createaccount/CreateAccountActivity.kt | 69 ++++++++++++++++++- gradle/libs.versions.toml | 2 +- 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d9b916ca990..458d085d352 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -86,9 +86,6 @@ - - - + // exception.hCaptchaError == HCaptchaError.SESSION_TIMEOUT + //} + .build()) + + hCaptcha?.addOnSuccessListener { response -> + tokenResponse = response + val userResponseToken = response.tokenResult + L.d("hCaptcha token: $userResponseToken") + finish() + }?.addOnFailureListener { e -> + L.e("hCaptcha failed: ${e.message} (${e.statusCode})") + tokenResponse = null + FeedbackUtil.showMessage(this, "hCaptcha failed: ${e.message} (${e.statusCode})") + }?.addOnOpenListener { + FeedbackUtil.showMessage(this, "hCaptcha shown") + } + } + hCaptcha?.verifyWithHCaptcha() + } + + private fun resetHCaptcha() { + hCaptcha?.reset() + hCaptcha = null + } + private fun handleAccountCreationError(message: String) { if (message.contains("blocked")) { FeedbackUtil.makeSnackbar(this, getString(R.string.create_account_ip_block_message)) @@ -210,6 +276,7 @@ class CreateAccountActivity : BaseActivity() { } public override fun onDestroy() { + resetHCaptcha() captchaHandler.dispose() userNameTextWatcher?.let { binding.createAccountUsername.editText?.removeTextChangedListener(it) } super.onDestroy() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 56e817c06ec..8ed96f01f3c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ googlePayVersion = "19.4.0" googleServices = "4.4.3" gradle = "8.13.0" hamcrest = "3.0" -hcaptcha = "4.0.3" +hcaptcha = "4.2.3" installreferrer = "2.2" jsoup = "1.21.2" junit = "4.13.2" From c30ec25e39f6771062be8925e5039048f9d8942b Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Fri, 19 Sep 2025 13:51:12 -0400 Subject: [PATCH 09/15] Yep. --- .../org/wikipedia/createaccount/CreateAccountActivity.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt index 4ceae8b84b4..99c781a25f9 100644 --- a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt +++ b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt @@ -203,11 +203,15 @@ class CreateAccountActivity : BaseActivity() { hCaptcha?.setup( HCaptchaConfig.builder() .siteKey("45205f58-be1c-40f0-b286-07a4498ea3da") + //.siteKey("f1f21d64-6384-4114-b7d0-d9d23e203b4a") .theme(if (WikipediaApp.instance.currentTheme.isDark) HCaptchaTheme.DARK else HCaptchaTheme.LIGHT) .size(HCaptchaSize.NORMAL) .host("meta.wikimedia.org") - .jsSrc("https://assets-hcaptcha.wikimedia.org/captcha/1/73f27c192b38c05ce2ebce596a0e28f88a2a56bf/secure-api.js") + + //.jsSrc("https://assets-hcaptcha.wikimedia.org/captcha/1/73f27c192b38c05ce2ebce596a0e28f88a2a56bf/secure-api.js") + .jsSrc("https://js.hcaptcha.com/1/api.js") + .endpoint("https://hcaptcha.wikimedia.org") .assethost("https://assets-hcaptcha.wikimedia.org") .imghost("https://imgs-hcaptcha.wikimedia.org") From c4f594a8a04b81987be7aa071c95024b4e43104c Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Mon, 22 Sep 2025 10:15:51 -0400 Subject: [PATCH 10/15] Close the loop. --- .../createaccount/CreateAccountActivity.kt | 24 +++++++++---------- .../CreateAccountActivityViewModel.kt | 15 ++++++++---- .../dataclient/mwapi/MwAuthManagerInfo.kt | 2 +- .../dataclient/mwapi/MwQueryResult.kt | 4 ++++ 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt index 99c781a25f9..30738f83e9c 100644 --- a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt +++ b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt @@ -100,6 +100,9 @@ class CreateAccountActivity : BaseActivity() { is CreateAccountActivityViewModel.AccountInfoState.DoCreateAccount -> { doCreateAccount(it.token) } + is CreateAccountActivityViewModel.AccountInfoState.HandleHCaptcha -> { + showHCaptcha() + } is CreateAccountActivityViewModel.AccountInfoState.HandleCaptcha -> { captchaHandler.handleCaptcha(it.token, CaptchaResult(it.captchaId)) } @@ -202,23 +205,17 @@ class CreateAccountActivity : BaseActivity() { hCaptcha = HCaptcha.getClient(this) hCaptcha?.setup( HCaptchaConfig.builder() - .siteKey("45205f58-be1c-40f0-b286-07a4498ea3da") - //.siteKey("f1f21d64-6384-4114-b7d0-d9d23e203b4a") + .siteKey("f1f21d64-6384-4114-b7d0-d9d23e203b4a") .theme(if (WikipediaApp.instance.currentTheme.isDark) HCaptchaTheme.DARK else HCaptchaTheme.LIGHT) - .size(HCaptchaSize.NORMAL) - .host("meta.wikimedia.org") - //.jsSrc("https://assets-hcaptcha.wikimedia.org/captcha/1/73f27c192b38c05ce2ebce596a0e28f88a2a56bf/secure-api.js") - .jsSrc("https://js.hcaptcha.com/1/api.js") - + .jsSrc("https://assets-hcaptcha.wikimedia.org/1/api.js") .endpoint("https://hcaptcha.wikimedia.org") .assethost("https://assets-hcaptcha.wikimedia.org") .imghost("https://imgs-hcaptcha.wikimedia.org") .reportapi("https://report-hcaptcha.wikimedia.org") .sentry(false) - //.loading(true) //.locale("en") //.size(HCaptchaSize.INVISIBLE) @@ -228,13 +225,12 @@ class CreateAccountActivity : BaseActivity() { //.retryPredicate { config, exception -> // exception.hCaptchaError == HCaptchaError.SESSION_TIMEOUT //} + .build()) hCaptcha?.addOnSuccessListener { response -> tokenResponse = response - val userResponseToken = response.tokenResult - L.d("hCaptcha token: $userResponseToken") - finish() + doCreateAccount(viewModel.token.orEmpty(), hCaptchaToken = response.tokenResult) }?.addOnFailureListener { e -> L.e("hCaptcha failed: ${e.message} (${e.statusCode})") tokenResponse = null @@ -265,13 +261,15 @@ class CreateAccountActivity : BaseActivity() { L.w("Account creation failed with result $message") } - private fun doCreateAccount(token: String) { + private fun doCreateAccount(token: String, hCaptchaToken: String? = null) { showProgressBar(true) val email = getText(binding.createAccountEmail).ifEmpty { null } val password = getText(binding.createAccountPasswordInput) val repeat = getText(binding.createAccountPasswordRepeat) val userName = getText(binding.createAccountUsername) - viewModel.doCreateAccount(token, captchaHandler.captchaId().toString(), captchaHandler.captchaWord().toString(), userName, password, repeat, email) + viewModel.doCreateAccount(token, captchaHandler.captchaId(), + if (hCaptchaToken.isNullOrEmpty()) captchaHandler.captchaWord() else hCaptchaToken, + userName, password, repeat, email) } public override fun onStop() { diff --git a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivityViewModel.kt b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivityViewModel.kt index 9a98ed5b718..b6f63c2e0f0 100644 --- a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivityViewModel.kt +++ b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivityViewModel.kt @@ -26,6 +26,8 @@ class CreateAccountActivityViewModel : ViewModel() { private val _verifyUserNameState = MutableSharedFlow() val verifyUserNameState = _verifyUserNameState.asSharedFlow() + var token: String? = null + private var verifyUserNameJob: Job? = null fun createAccountInfo() { @@ -33,19 +35,21 @@ class CreateAccountActivityViewModel : ViewModel() { _createAccountInfoState.value = AccountInfoState.Error(throwable) }) { val response = ServiceFactory.get(WikipediaApp.instance.wikiSite).getAuthManagerInfo() - val token = response.query?.createAccountToken() + token = response.query?.createAccountToken() val captchaId = response.query?.captchaId() if (token.isNullOrEmpty()) { _createAccountInfoState.value = AccountInfoState.InvalidToken + } else if (response.query?.hasHCaptchaRequest() == true) { + _createAccountInfoState.value = AccountInfoState.HandleHCaptcha(token!!) } else if (!captchaId.isNullOrEmpty()) { - _createAccountInfoState.value = AccountInfoState.HandleCaptcha(token, captchaId) + _createAccountInfoState.value = AccountInfoState.HandleCaptcha(token!!, captchaId) } else { - _createAccountInfoState.value = AccountInfoState.DoCreateAccount(token) + _createAccountInfoState.value = AccountInfoState.DoCreateAccount(token!!) } } } - fun doCreateAccount(token: String, captchaId: String, captchaWord: String, userName: String, password: String, repeat: String, email: String?) { + fun doCreateAccount(token: String, captchaId: String?, captchaWord: String?, userName: String, password: String, repeat: String, email: String?) { viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> _doCreateAccountState.value = CreateAccountState.Error(throwable) }) { @@ -84,7 +88,8 @@ class CreateAccountActivityViewModel : ViewModel() { open class AccountInfoState { data class DoCreateAccount(val token: String) : AccountInfoState() - data class HandleCaptcha(val token: String?, val captchaId: String) : AccountInfoState() + data class HandleCaptcha(val token: String, val captchaId: String) : AccountInfoState() + data class HandleHCaptcha(val token: String) : AccountInfoState() data object InvalidToken : AccountInfoState() data class Error(val throwable: Throwable) : AccountInfoState() } diff --git a/app/src/main/java/org/wikipedia/dataclient/mwapi/MwAuthManagerInfo.kt b/app/src/main/java/org/wikipedia/dataclient/mwapi/MwAuthManagerInfo.kt index 24faf1ab0a0..d75599747ba 100644 --- a/app/src/main/java/org/wikipedia/dataclient/mwapi/MwAuthManagerInfo.kt +++ b/app/src/main/java/org/wikipedia/dataclient/mwapi/MwAuthManagerInfo.kt @@ -11,7 +11,7 @@ internal class MwAuthManagerInfo { internal class Request(val id: String? = null, private val metadata: Map? = null, private val required: String? = null, - private val provider: String? = null, + val provider: String? = null, private val account: String? = null, val fields: Map? = null) diff --git a/app/src/main/java/org/wikipedia/dataclient/mwapi/MwQueryResult.kt b/app/src/main/java/org/wikipedia/dataclient/mwapi/MwQueryResult.kt index 428134591f7..59e3791c245 100644 --- a/app/src/main/java/org/wikipedia/dataclient/mwapi/MwQueryResult.kt +++ b/app/src/main/java/org/wikipedia/dataclient/mwapi/MwQueryResult.kt @@ -79,6 +79,10 @@ class MwQueryResult { return amInfo?.requests?.find { it.fields?.containsKey(key) == true }?.fields?.get(key)?.value } + fun hasHCaptchaRequest(): Boolean { + return amInfo?.requests?.find { it.provider.orEmpty().lowercase().contains("hcaptcha") } != null + } + fun getUserResponse(userName: String): UserInfo? { // MediaWiki user names are case sensitive, but the first letter is always capitalized. return users?.find { StringUtil.capitalize(userName) == it.name } From 2a1e4f866cb8e0dbd49cd68834b6e3349706d5ff Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Tue, 23 Sep 2025 07:42:38 -0400 Subject: [PATCH 11/15] Refactor into helper class. --- .../org/wikipedia/captcha/HCaptchaHelper.kt | 70 +++++++++++++++++++ .../createaccount/CreateAccountActivity.kt | 65 ++--------------- 2 files changed, 77 insertions(+), 58 deletions(-) create mode 100644 app/src/main/java/org/wikipedia/captcha/HCaptchaHelper.kt diff --git a/app/src/main/java/org/wikipedia/captcha/HCaptchaHelper.kt b/app/src/main/java/org/wikipedia/captcha/HCaptchaHelper.kt new file mode 100644 index 00000000000..6d9b814b0b9 --- /dev/null +++ b/app/src/main/java/org/wikipedia/captcha/HCaptchaHelper.kt @@ -0,0 +1,70 @@ +package org.wikipedia.captcha + +import androidx.fragment.app.FragmentActivity +import com.hcaptcha.sdk.HCaptcha +import com.hcaptcha.sdk.HCaptchaConfig +import com.hcaptcha.sdk.HCaptchaTheme +import com.hcaptcha.sdk.HCaptchaTokenResponse +import org.wikipedia.WikipediaApp +import org.wikipedia.util.FeedbackUtil +import org.wikipedia.util.log.L + +class HCaptchaHelper( + private val activity: FragmentActivity, + private val callback: Callback +) { + fun interface Callback { + fun onSuccess(token: String) + } + + private var hCaptcha: HCaptcha? = null + private var tokenResponse: HCaptchaTokenResponse? = null + + + fun show() { + if (hCaptcha == null) { + hCaptcha = HCaptcha.getClient(activity) + hCaptcha?.setup( + HCaptchaConfig.builder() + .siteKey("f1f21d64-6384-4114-b7d0-d9d23e203b4a") + .theme(if (WikipediaApp.instance.currentTheme.isDark) HCaptchaTheme.DARK else HCaptchaTheme.LIGHT) + .host("meta.wikimedia.org") + + .jsSrc("https://assets-hcaptcha.wikimedia.org/1/api.js") + .endpoint("https://hcaptcha.wikimedia.org") + .assethost("https://assets-hcaptcha.wikimedia.org") + .imghost("https://imgs-hcaptcha.wikimedia.org") + .reportapi("https://report-hcaptcha.wikimedia.org") + .sentry(false) + + //.loading(true) + //.locale("en") + //.size(HCaptchaSize.INVISIBLE) + //.hideDialog(false) + //.tokenExpiration(10) + //.diagnosticLog(true) + //.retryPredicate { config, exception -> + // exception.hCaptchaError == HCaptchaError.SESSION_TIMEOUT + //} + + .build()) + + hCaptcha?.addOnSuccessListener { response -> + tokenResponse = response + callback.onSuccess(response.tokenResult) + }?.addOnFailureListener { e -> + tokenResponse = null + L.e("hCaptcha failed: ${e.message} (${e.statusCode})") + FeedbackUtil.showMessage(activity, "${e.message} (${e.statusCode})") + }?.addOnOpenListener { + L.d("hCaptcha opened") + } + } + hCaptcha?.verifyWithHCaptcha() + } + + fun cleanup() { + hCaptcha?.reset() + hCaptcha = null + } +} \ No newline at end of file diff --git a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt index 30738f83e9c..daf033a5e36 100644 --- a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt +++ b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt @@ -19,11 +19,6 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.TextInputLayout -import com.hcaptcha.sdk.HCaptcha -import com.hcaptcha.sdk.HCaptchaConfig -import com.hcaptcha.sdk.HCaptchaSize -import com.hcaptcha.sdk.HCaptchaTheme -import com.hcaptcha.sdk.HCaptchaTokenResponse import kotlinx.coroutines.launch import org.wikipedia.R import org.wikipedia.WikipediaApp @@ -32,6 +27,7 @@ import org.wikipedia.analytics.eventplatform.CreateAccountEvent import org.wikipedia.auth.AccountUtil import org.wikipedia.captcha.CaptchaHandler import org.wikipedia.captcha.CaptchaResult +import org.wikipedia.captcha.HCaptchaHelper import org.wikipedia.databinding.ActivityCreateAccountBinding import org.wikipedia.page.PageTitle import org.wikipedia.util.DeviceUtil @@ -54,8 +50,9 @@ class CreateAccountActivity : BaseActivity() { private var userNameTextWatcher: TextWatcher? = null private val viewModel: CreateAccountActivityViewModel by viewModels() - private var hCaptcha: HCaptcha? = null - private var tokenResponse: HCaptchaTokenResponse? = null + private val hCaptchaHelper = HCaptchaHelper(this) { token -> + doCreateAccount(viewModel.token.orEmpty(), hCaptchaToken = token) + } public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -101,7 +98,7 @@ class CreateAccountActivity : BaseActivity() { doCreateAccount(it.token) } is CreateAccountActivityViewModel.AccountInfoState.HandleHCaptcha -> { - showHCaptcha() + hCaptchaHelper.show() } is CreateAccountActivityViewModel.AccountInfoState.HandleCaptcha -> { captchaHandler.handleCaptcha(it.token, CaptchaResult(it.captchaId)) @@ -177,7 +174,7 @@ class CreateAccountActivity : BaseActivity() { - showHCaptcha() + hCaptchaHelper.show() @@ -199,54 +196,6 @@ class CreateAccountActivity : BaseActivity() { } } - private fun showHCaptcha() { - if (hCaptcha == null) { - - hCaptcha = HCaptcha.getClient(this) - hCaptcha?.setup( - HCaptchaConfig.builder() - .siteKey("f1f21d64-6384-4114-b7d0-d9d23e203b4a") - .theme(if (WikipediaApp.instance.currentTheme.isDark) HCaptchaTheme.DARK else HCaptchaTheme.LIGHT) - .host("meta.wikimedia.org") - - .jsSrc("https://assets-hcaptcha.wikimedia.org/1/api.js") - .endpoint("https://hcaptcha.wikimedia.org") - .assethost("https://assets-hcaptcha.wikimedia.org") - .imghost("https://imgs-hcaptcha.wikimedia.org") - .reportapi("https://report-hcaptcha.wikimedia.org") - .sentry(false) - - //.loading(true) - //.locale("en") - //.size(HCaptchaSize.INVISIBLE) - //.hideDialog(false) - //.tokenExpiration(10) - //.diagnosticLog(true) - //.retryPredicate { config, exception -> - // exception.hCaptchaError == HCaptchaError.SESSION_TIMEOUT - //} - - .build()) - - hCaptcha?.addOnSuccessListener { response -> - tokenResponse = response - doCreateAccount(viewModel.token.orEmpty(), hCaptchaToken = response.tokenResult) - }?.addOnFailureListener { e -> - L.e("hCaptcha failed: ${e.message} (${e.statusCode})") - tokenResponse = null - FeedbackUtil.showMessage(this, "hCaptcha failed: ${e.message} (${e.statusCode})") - }?.addOnOpenListener { - FeedbackUtil.showMessage(this, "hCaptcha shown") - } - } - hCaptcha?.verifyWithHCaptcha() - } - - private fun resetHCaptcha() { - hCaptcha?.reset() - hCaptcha = null - } - private fun handleAccountCreationError(message: String) { if (message.contains("blocked")) { FeedbackUtil.makeSnackbar(this, getString(R.string.create_account_ip_block_message)) @@ -278,7 +227,7 @@ class CreateAccountActivity : BaseActivity() { } public override fun onDestroy() { - resetHCaptcha() + hCaptchaHelper.cleanup() captchaHandler.dispose() userNameTextWatcher?.let { binding.createAccountUsername.editText?.removeTextChangedListener(it) } super.onDestroy() From a7fe9c22a7954d663b9716a4ddcc772b03b2d3f3 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 24 Sep 2025 09:53:43 -0400 Subject: [PATCH 12/15] Clean up. --- app/src/main/java/org/wikipedia/settings/AboutActivity.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/org/wikipedia/settings/AboutActivity.kt b/app/src/main/java/org/wikipedia/settings/AboutActivity.kt index 7eee94450cb..662df0fc7f1 100644 --- a/app/src/main/java/org/wikipedia/settings/AboutActivity.kt +++ b/app/src/main/java/org/wikipedia/settings/AboutActivity.kt @@ -94,7 +94,6 @@ class AboutActivity : BaseActivity() { asset = "licenses/Retrofit" ) ) - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) DeviceUtil.setEdgeToEdge(this) From 7331a7b4240eaa79ddd7d95b1734b5f497161eab Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 24 Sep 2025 09:56:00 -0400 Subject: [PATCH 13/15] Put vals into constructor. --- .../org/wikipedia/settings/RemoteConfig.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/wikipedia/settings/RemoteConfig.kt b/app/src/main/java/org/wikipedia/settings/RemoteConfig.kt index a0eb1a78037..d3ffc5d04f4 100644 --- a/app/src/main/java/org/wikipedia/settings/RemoteConfig.kt +++ b/app/src/main/java/org/wikipedia/settings/RemoteConfig.kt @@ -49,14 +49,14 @@ object RemoteConfig { @Suppress("unused") @Serializable - class RemoteConfigHCaptcha { - val baseURL = "" - val jsSrc = "" - val endpoint = "" - @SerialName("assethost") val assetHost = "" - @SerialName("imghost") val imgHost = "" - @SerialName("reportapi") val reportApi = "" - val sentry = false - val siteKey = "" - } + class RemoteConfigHCaptcha( + val baseURL: String = "", + val jsSrc: String = "", + val endpoint: String = "", + @SerialName("assethost") val assetHost: String = "", + @SerialName("imghost") val imgHost: String = "", + @SerialName("reportapi") val reportApi: String = "", + val sentry: Boolean = false, + val siteKey: String = "" + ) } From 4db3becb6c8e1fd270224287c4e445b2f1f13635 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 24 Sep 2025 10:16:22 -0400 Subject: [PATCH 14/15] Wire up to remote config. --- .../org/wikipedia/captcha/HCaptchaHelper.kt | 45 ++++++++++--------- .../createaccount/CreateAccountActivity.kt | 1 + 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/org/wikipedia/captcha/HCaptchaHelper.kt b/app/src/main/java/org/wikipedia/captcha/HCaptchaHelper.kt index 6d9b814b0b9..1879f8cec88 100644 --- a/app/src/main/java/org/wikipedia/captcha/HCaptchaHelper.kt +++ b/app/src/main/java/org/wikipedia/captcha/HCaptchaHelper.kt @@ -1,13 +1,16 @@ package org.wikipedia.captcha +import androidx.core.net.toUri import androidx.fragment.app.FragmentActivity import com.hcaptcha.sdk.HCaptcha import com.hcaptcha.sdk.HCaptchaConfig import com.hcaptcha.sdk.HCaptchaTheme import com.hcaptcha.sdk.HCaptchaTokenResponse import org.wikipedia.WikipediaApp +import org.wikipedia.settings.RemoteConfig import org.wikipedia.util.FeedbackUtil import org.wikipedia.util.log.L +import kotlin.String class HCaptchaHelper( private val activity: FragmentActivity, @@ -20,33 +23,33 @@ class HCaptchaHelper( private var hCaptcha: HCaptcha? = null private var tokenResponse: HCaptchaTokenResponse? = null + private val configDefault get() = RemoteConfig.RemoteConfigHCaptcha( + baseURL = "https://meta.wikimedia.org", + jsSrc = "https://assets-hcaptcha.wikimedia.org/1/api.js", + endpoint = "https://hcaptcha.wikimedia.org", + assetHost = "https://assets-hcaptcha.wikimedia.org", + imgHost = "https://imgs-hcaptcha.wikimedia.org", + reportApi = "https://report-hcaptcha.wikimedia.org", + sentry = false, + siteKey = "f1f21d64-6384-4114-b7d0-d9d23e203b4a" + ) fun show() { if (hCaptcha == null) { + val config = RemoteConfig.config.androidv1?.hCaptcha ?: configDefault hCaptcha = HCaptcha.getClient(activity) hCaptcha?.setup( HCaptchaConfig.builder() - .siteKey("f1f21d64-6384-4114-b7d0-d9d23e203b4a") .theme(if (WikipediaApp.instance.currentTheme.isDark) HCaptchaTheme.DARK else HCaptchaTheme.LIGHT) - .host("meta.wikimedia.org") - - .jsSrc("https://assets-hcaptcha.wikimedia.org/1/api.js") - .endpoint("https://hcaptcha.wikimedia.org") - .assethost("https://assets-hcaptcha.wikimedia.org") - .imghost("https://imgs-hcaptcha.wikimedia.org") - .reportapi("https://report-hcaptcha.wikimedia.org") - .sentry(false) - - //.loading(true) - //.locale("en") - //.size(HCaptchaSize.INVISIBLE) - //.hideDialog(false) - //.tokenExpiration(10) - //.diagnosticLog(true) - //.retryPredicate { config, exception -> - // exception.hCaptchaError == HCaptchaError.SESSION_TIMEOUT - //} - + .siteKey(config.siteKey) + .host(config.baseURL.toUri().host) + .jsSrc(config.jsSrc) + .endpoint(config.endpoint) + .assethost(config.assetHost) + .imghost(config.imgHost) + .reportapi(config.reportApi) + .sentry(config.sentry) + .loading(true) .build()) hCaptcha?.addOnSuccessListener { response -> @@ -67,4 +70,4 @@ class HCaptchaHelper( hCaptcha?.reset() hCaptcha = null } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt index daf033a5e36..272514831e5 100644 --- a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt +++ b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt @@ -174,6 +174,7 @@ class CreateAccountActivity : BaseActivity() { + // TODO: remove when done testing hCaptchaHelper.show() From aa290b771fd35bfd876b2549454a21865a92fb60 Mon Sep 17 00:00:00 2001 From: Dmitry Brant Date: Wed, 1 Oct 2025 09:58:31 -0400 Subject: [PATCH 15/15] Clean up. --- .../main/java/org/wikipedia/captcha/HCaptchaHelper.kt | 2 +- .../wikipedia/createaccount/CreateAccountActivity.kt | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/wikipedia/captcha/HCaptchaHelper.kt b/app/src/main/java/org/wikipedia/captcha/HCaptchaHelper.kt index 1879f8cec88..4be62238934 100644 --- a/app/src/main/java/org/wikipedia/captcha/HCaptchaHelper.kt +++ b/app/src/main/java/org/wikipedia/captcha/HCaptchaHelper.kt @@ -31,7 +31,7 @@ class HCaptchaHelper( imgHost = "https://imgs-hcaptcha.wikimedia.org", reportApi = "https://report-hcaptcha.wikimedia.org", sentry = false, - siteKey = "f1f21d64-6384-4114-b7d0-d9d23e203b4a" + siteKey = "e11698d6-51ca-4980-875c-72309c6678cc" ) fun show() { diff --git a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt index 272514831e5..4337f47ac10 100644 --- a/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt +++ b/app/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.kt @@ -171,15 +171,7 @@ class CreateAccountActivity : BaseActivity() { finish() } binding.footerContainer.privacyPolicyLink.setOnClickListener { - - - - // TODO: remove when done testing - hCaptchaHelper.show() - - - - //FeedbackUtil.showPrivacyPolicy(this) + FeedbackUtil.showPrivacyPolicy(this) } binding.footerContainer.forgotPasswordLink.setOnClickListener { visitInExternalBrowser(this, PageTitle("Special:PasswordReset", wiki).uri.toUri())