From 110fa881b3e1be3868ee140a1cdf18ea0312efd9 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 25 Aug 2026 13:16:42 +0530 Subject: [PATCH 01/10] refactor(ccct-2709): extract BasePersonalIdBackupCodeFragment from PersonalIdBackupCodeFragment --- .../BasePersonalIdBackupCodeFragment.kt | 68 +++++++++++++++++++ .../PersonalIdBackupCodeFragment.kt | 63 +++-------------- 2 files changed, 77 insertions(+), 54 deletions(-) create mode 100644 app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt diff --git a/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt b/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt new file mode 100644 index 0000000000..84f11e893f --- /dev/null +++ b/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt @@ -0,0 +1,68 @@ +package org.commcare.fragments.personalId + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import org.commcare.dalvik.R +import org.commcare.dalvik.databinding.FragmentRecoveryCodeBinding +import org.commcare.views.connect.NumericCodeView + +abstract class BasePersonalIdBackupCodeFragment : BasePersonalIdFragment() { + protected lateinit var binding: FragmentRecoveryCodeBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + binding = FragmentRecoveryCodeBinding.inflate(inflater, container, false) + clearBackupCodeFields() + return binding.root + } + + override fun onResume() { + super.onResume() + binding.backupCodeView.requestFocus(requireActivity()) + } + + protected fun clearBackupCodeFields() { + binding.backupCodeView.clearCode() + binding.confirmCodeView.clearCode() + } + + protected open fun submitIfEnabled() { + if (binding.connectBackupCodeButton.isEnabled) handleBackupCodeSubmission() + } + + abstract fun handleBackupCodeSubmission() + + protected fun togglePasswordVisibility( + codeView: NumericCodeView, + toggle: ImageView, + ) { + codeView.isPasswordVisible = !codeView.isPasswordVisible + toggle.setImageResource( + if (codeView.isPasswordVisible) R.drawable.ic_visibility_off_24 else R.drawable.ic_visibility_24, + ) + } + + protected fun clearError() { + binding.connectBackupCodeErrorMessage.visibility = View.GONE + binding.connectBackupCodeErrorMessage.text = "" + } + + protected fun showError(message: String) { + binding.connectBackupCodeErrorMessage.visibility = View.VISIBLE + binding.connectBackupCodeErrorMessage.text = message + } + + protected fun enableContinueButton(enabled: Boolean) { + binding.connectBackupCodeButton.isEnabled = enabled + } + + companion object { + const val BACKUP_CODE_LENGTH = 6 + } +} diff --git a/app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.kt b/app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.kt index 3b3a794ccd..86739edd72 100644 --- a/app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.kt +++ b/app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.kt @@ -4,7 +4,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageView import androidx.annotation.StringRes import androidx.lifecycle.ViewModelProvider import androidx.navigation.NavDirections @@ -17,7 +16,6 @@ import org.commcare.connect.network.base.BaseApiHandler.PersonalIdOrConnectApiEr import org.commcare.connect.network.base.PersonalIdOrConnectApiErrorHandler import org.commcare.connect.network.personalId.PersonalIdApiHandler import org.commcare.dalvik.R -import org.commcare.dalvik.databinding.FragmentRecoveryCodeBinding import org.commcare.google.services.analytics.AnalyticsParamValue import org.commcare.google.services.analytics.FirebaseAnalyticsUtil import org.commcare.personalId.PersonalIdRecoveryCompleter @@ -26,34 +24,31 @@ import org.commcare.utils.MediaUtil import org.commcare.views.connect.NumericCodeView import java.util.Date -class PersonalIdBackupCodeFragment : BasePersonalIdFragment() { - private lateinit var binding: FragmentRecoveryCodeBinding +class PersonalIdBackupCodeFragment : BasePersonalIdBackupCodeFragment() { private lateinit var personalIdSessionData: PersonalIdSessionData private var isRecovery = false @StringRes private var titleId = 0 - override fun onResume() { - super.onResume() - validateBackupCodeInputs() - binding.backupCodeView.requestFocus(requireActivity()) - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View { - binding = FragmentRecoveryCodeBinding.inflate(inflater, container, false) + val view = super.onCreateView(inflater, container, savedInstanceState) personalIdSessionData = ViewModelProvider(requireActivity())[PersonalIdSessionDataViewModel::class.java] .personalIdSessionData configureUiByMode() setupListeners() - clearBackupCodeFields() requireActivity().title = getString(titleId) - return binding.root + return view + } + + override fun onResume() { + super.onResume() + validateBackupCodeInputs() } private fun configureUiByMode() { @@ -113,27 +108,6 @@ class PersonalIdBackupCodeFragment : BasePersonalIdFragment() { } } - private fun submitIfEnabled() { - if (binding.connectBackupCodeButton.isEnabled) { - handleBackupCodeSubmission() - } - } - - private fun togglePasswordVisibility( - codeView: NumericCodeView, - toggle: ImageView, - ) { - codeView.isPasswordVisible = !codeView.isPasswordVisible - toggle.setImageResource( - if (codeView.isPasswordVisible) R.drawable.ic_visibility_off_24 else R.drawable.ic_visibility_24, - ) - } - - private fun clearBackupCodeFields() { - binding.confirmCodeView.clearCode() - binding.backupCodeView.clearCode() - } - private fun validateBackupCodeInputs() { val backupCode = binding.backupCodeView.codeValue val confirmCode = binding.confirmCodeView.codeValue @@ -154,11 +128,7 @@ class PersonalIdBackupCodeFragment : BasePersonalIdFragment() { configureUiByMode() } - private fun enableContinueButton(isEnable: Boolean) { - binding.connectBackupCodeButton.isEnabled = isEnable - } - - private fun handleBackupCodeSubmission() { + override fun handleBackupCodeSubmission() { FirebaseAnalyticsUtil.reportPersonalIDContinueClicked( javaClass.simpleName, null, @@ -216,16 +186,6 @@ class PersonalIdBackupCodeFragment : BasePersonalIdFragment() { } } - private fun clearError() { - binding.connectBackupCodeErrorMessage.visibility = View.GONE - binding.connectBackupCodeErrorMessage.text = "" - } - - private fun showError(message: String) { - binding.connectBackupCodeErrorMessage.visibility = View.VISIBLE - binding.connectBackupCodeErrorMessage.text = message - } - private fun handleFailedBackupCodeAttempt() { logRecoveryFailureResult() clearBackupCodeFields() @@ -292,9 +252,4 @@ class PersonalIdBackupCodeFragment : BasePersonalIdFragment() { } private fun navigate(directions: NavDirections) = binding.root.findNavController().navigate(directions) - - companion object { - // Note: This is brittle, defined in several places - private const val BACKUP_CODE_LENGTH = 6 - } } From 7106faa06e9ae3aa094e81b0adfccd6f6c0a788f Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 25 Aug 2026 13:21:39 +0530 Subject: [PATCH 02/10] feat(ccct-2709): add profile backup-code entry point and confirm-current-backup-code screen --- app/res/drawable/ic_key_round.xml | 9 ++ app/res/layout/fragment_recovery_code.xml | 27 ++-- app/res/layout/personalid_profile_screen.xml | 43 ++++-- .../nav_graph_personalid_profile.xml | 21 +++ app/res/values-es/strings.xml | 4 + app/res/values-fr/strings.xml | 4 + app/res/values-ha/strings.xml | 4 + app/res/values-hi/strings.xml | 3 + app/res/values-lt/strings.xml | 3 + app/res/values-no/strings.xml | 3 + app/res/values-pt/strings.xml | 4 + app/res/values-sw/strings.xml | 3 + app/res/values-ti/strings.xml | 4 + app/res/values/strings.xml | 4 + .../personalId/BackupCodeWorkflow.kt | 11 ++ .../PersonalIdProfileBackupCodeFragment.kt | 98 ++++++++++++ .../profile/PersonalIdProfileFragment.kt | 3 + .../profile/SetNewBackupCodeFragment.kt | 16 ++ ...PersonalIdProfileBackupCodeFragmentTest.kt | 143 ++++++++++++++++++ .../profile/PersonalIdProfileFragmentTest.kt | 10 ++ 20 files changed, 390 insertions(+), 27 deletions(-) create mode 100644 app/res/drawable/ic_key_round.xml create mode 100644 app/src/org/commcare/fragments/personalId/BackupCodeWorkflow.kt create mode 100644 app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt create mode 100644 app/src/org/commcare/personalId/profile/SetNewBackupCodeFragment.kt create mode 100644 app/unit-tests/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragmentTest.kt diff --git a/app/res/drawable/ic_key_round.xml b/app/res/drawable/ic_key_round.xml new file mode 100644 index 0000000000..d3cbdaef06 --- /dev/null +++ b/app/res/drawable/ic_key_round.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/res/layout/fragment_recovery_code.xml b/app/res/layout/fragment_recovery_code.xml index 50608bf892..cebc632874 100644 --- a/app/res/layout/fragment_recovery_code.xml +++ b/app/res/layout/fragment_recovery_code.xml @@ -212,23 +212,22 @@ android:textSize="16sp" android:visibility="gone"/> - + android:layout_height="wrap_content" + android:orientation="vertical"> @@ -237,17 +236,13 @@ style="@style/CustomButtonStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginTop="16dp" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintBottom_toBottomOf="parent" + android:layout_gravity="end" + android:layout_marginTop="32dp" android:layout_marginEnd="16dp" android:layout_marginBottom="16dp" android:text="@string/personalid_continue_label" - android:drawableRight="@drawable/connect_right_arrow" - /> - + android:drawableRight="@drawable/connect_right_arrow"/> + diff --git a/app/res/layout/personalid_profile_screen.xml b/app/res/layout/personalid_profile_screen.xml index f464b2e52d..161566a824 100644 --- a/app/res/layout/personalid_profile_screen.xml +++ b/app/res/layout/personalid_profile_screen.xml @@ -91,20 +91,41 @@ android:layout_marginTop="16dp" android:background="@color/grey_light" /> - + android:layout_marginTop="32dp" + android:orientation="vertical"> + + + + + + diff --git a/app/res/navigation/nav_graph_personalid_profile.xml b/app/res/navigation/nav_graph_personalid_profile.xml index a7a424c5cf..01b063ee90 100644 --- a/app/res/navigation/nav_graph_personalid_profile.xml +++ b/app/res/navigation/nav_graph_personalid_profile.xml @@ -12,6 +12,9 @@ + + + + + + + + + diff --git a/app/res/values-es/strings.xml b/app/res/values-es/strings.xml index 05d6a5b0d9..20295a25cb 100644 --- a/app/res/values-es/strings.xml +++ b/app/res/values-es/strings.xml @@ -415,6 +415,10 @@ Guardar Cancelar Tu perfil se ha actualizado correctamente. + Cambiar código de respaldo + ¿Olvidaste tu código de respaldo? + Por favor, añade una dirección de correo electrónico + Establecer nuevo código de respaldo Detalles de la entrega Revisar detalles de la entrega %d visitas máximo diff --git a/app/res/values-fr/strings.xml b/app/res/values-fr/strings.xml index b2ac2d4897..86f45ef2d4 100644 --- a/app/res/values-fr/strings.xml +++ b/app/res/values-fr/strings.xml @@ -412,6 +412,10 @@ License. Enregistrer Annuler Votre profil a été mis à jour avec succès. + Changer le code de sauvegarde + Vous avez oublié votre code de sauvegarde ? + Veuillez ajouter une adresse e-mail + Définir un nouveau code de sauvegarde Détails de livraison Vérifiez les détails de livraison %d visites maximum diff --git a/app/res/values-ha/strings.xml b/app/res/values-ha/strings.xml index 8f7b424004..12c29a37f6 100644 --- a/app/res/values-ha/strings.xml +++ b/app/res/values-ha/strings.xml @@ -250,6 +250,10 @@ Don cikakken aiki na wurin zama na aikace-aikace da yawa, da fatan za a yi sabon Ajiye Soke An sabunta bayanan martabarka cikin nasara. + Canza lambar sirrin ajiyar bayanai + Ka manta da lambar sirrin ajiyar bayananka? + Da fatan za a ƙara adireshin imel + Saita sabuwar lambar sirrin ajiyar bayanai Cikakkun Bayanan Isarwa Yi bitar cikakkun bayanai game da isar da kaya Matsakaicin Ziyarar %d diff --git a/app/res/values-hi/strings.xml b/app/res/values-hi/strings.xml index f1c42e5b17..9fa0fe2c7d 100644 --- a/app/res/values-hi/strings.xml +++ b/app/res/values-hi/strings.xml @@ -412,6 +412,9 @@ License. सहेजें रद्द करें आपकी प्रोफ़ाइल सफलतापूर्वक अपडेट कर दी गई है। + बैकअप कोड बदलें + अपना बैकअप कोड भूल गए? + Please add an email address वितरण विवरण वितरण विवरण की समीक्षा करें अधिकतम %d विज़िट diff --git a/app/res/values-lt/strings.xml b/app/res/values-lt/strings.xml index 5d5248c679..efeaf43207 100644 --- a/app/res/values-lt/strings.xml +++ b/app/res/values-lt/strings.xml @@ -103,6 +103,9 @@ Išsaugoti Atšaukti Jūsų profilis sėkmingai atnaujintas. + Keisti atsarginį kodą + Pamiršote savo atsarginį kodą? + Please add an email address Prisijungti / Registruotis Pagalba Neprisijungta prie „PersonalID“ diff --git a/app/res/values-no/strings.xml b/app/res/values-no/strings.xml index 62b8105c25..e5b1669bb6 100644 --- a/app/res/values-no/strings.xml +++ b/app/res/values-no/strings.xml @@ -103,6 +103,9 @@ Lagre Avbryt Profilen din er oppdatert. + Endre sikkerhetskode + Glemt sikkerhetskoden din? + Please add an email address Logg inn / Registrer deg Hjelp Du er ikke logget inn med PersonalID diff --git a/app/res/values-pt/strings.xml b/app/res/values-pt/strings.xml index 19f806aff8..c9b268f606 100644 --- a/app/res/values-pt/strings.xml +++ b/app/res/values-pt/strings.xml @@ -428,6 +428,10 @@ Salvar Cancelar Seu perfil foi atualizado com sucesso. + Alterar código de backup + Esqueceu o seu código de backup? + Por favor, adicione um endereço de e-mail + Definir novo código de backup Detalhes da entrega Revise os detalhes da entrega %d máximo de visitas diff --git a/app/res/values-sw/strings.xml b/app/res/values-sw/strings.xml index 44fbcd846a..ce8dcbeed9 100644 --- a/app/res/values-sw/strings.xml +++ b/app/res/values-sw/strings.xml @@ -414,6 +414,9 @@ Hifadhi Ghairi Wasifu wako umesasishwa kwa mafanikio. + Badilisha nambari ya hifadhi nakala + Umesahau nambari yako ya hifadhi nakala? + Please add an email address Maelezo ya Uwasilishaji Kagua maelezo ya utoaji %d upeo wa Ziara diff --git a/app/res/values-ti/strings.xml b/app/res/values-ti/strings.xml index 8016c118cd..2e1fec5a20 100644 --- a/app/res/values-ti/strings.xml +++ b/app/res/values-ti/strings.xml @@ -415,6 +415,10 @@ ኣቐምጥ ሰርዝ ፕሮፋይልኩም ብዓወት ተሐዲሱ። + ናይ ምትካእ ኮድ ቀይር + ናይ ምትካእ ኮድካ ረሲዕካ? + በጃኻ ናይ ኢ-መይል ኣድራሻ ወስኽ + ሓድሽ ናይ ምትካእ ኮድ ኣቐምጥ ዝርዝር (ኣተገባብራ)ኣወሃህባ ዝርዝር ኣወሃህባ ዳህሳስ ከልስ %d ዝለዓለ ምብጻሕ diff --git a/app/res/values/strings.xml b/app/res/values/strings.xml index 9e0558b24f..6be125f0be 100644 --- a/app/res/values/strings.xml +++ b/app/res/values/strings.xml @@ -378,6 +378,7 @@ Enter your 6-digit Backup Code Please be sure to remember your backup code! It must be %d digits. Keep it in a safe place as you will need it to access your account again. The backup codes you enter must match. + Set New Backup Code Wrong Backup Code Error parsing data from server. Please contact customer support. @@ -797,6 +798,9 @@ Take Photo Save Photo This is not me + Change backup code + Forgot your backup code? + Please add an email address Welcome back %s You have entered the wrong Backup Code. Please try again. Your account will be locked after %d more incorrect attempts. Login on Second Device diff --git a/app/src/org/commcare/fragments/personalId/BackupCodeWorkflow.kt b/app/src/org/commcare/fragments/personalId/BackupCodeWorkflow.kt new file mode 100644 index 0000000000..362c8b681a --- /dev/null +++ b/app/src/org/commcare/fragments/personalId/BackupCodeWorkflow.kt @@ -0,0 +1,11 @@ +package org.commcare.fragments.personalId + +/** + * Identifies which backup-code flow is being executed. + * + * - [CONFIRM_BACKUP_CODE_CHANGE_CODE]: user is on the Manage Profile screen and wants to change + * their backup code; must confirm the current code first before setting a new one. + */ +enum class BackupCodeWorkflow { + CONFIRM_BACKUP_CODE_CHANGE_CODE, +} diff --git a/app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt b/app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt new file mode 100644 index 0000000000..c8c68250ae --- /dev/null +++ b/app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt @@ -0,0 +1,98 @@ +package org.commcare.personalId.profile + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.navigation.fragment.findNavController +import org.commcare.connect.database.ConnectUserDatabaseUtil +import org.commcare.dalvik.R +import org.commcare.fragments.personalId.BasePersonalIdBackupCodeFragment +import org.commcare.fragments.personalId.EmailWorkFlow + +class PersonalIdProfileBackupCodeFragment : BasePersonalIdBackupCodeFragment() { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + val view = super.onCreateView(inflater, container, savedInstanceState) + setUpView() + return view + } + + fun setUpView() { + requireActivity().title = getString(R.string.connect_backup_code_title_confirm) + binding.recoveryCodeTilte.setText(R.string.connect_backup_code_title_confirm) + binding.backupCodeSubtitle.setText(R.string.connect_backup_code_message) + binding.backupCodeLayout.visibility = View.VISIBLE + binding.confirmCodeLayout.visibility = View.GONE + binding.confirmCodeLabel.visibility = View.GONE + binding.welcomeBackLayout.visibility = View.GONE + binding.notMeButton.visibility = View.VISIBLE + binding.notMeButton.setText(R.string.personalid_forgot_backup_code) + enableContinueButton(false) + setupListeners() + } + + private fun setupListeners() { + binding.backupCodeView.setOnCodeChangedListener { validateCode() } + binding.backupCodeView.setOnEnterKeyPressedListener { submitIfEnabled() } + binding.connectBackupCodeButton.setOnClickListener { handleBackupCodeSubmission() } + binding.notMeButton.setOnClickListener { handleForgot() } + binding.backupCodeVisibilityToggle.setOnClickListener { + togglePasswordVisibility(binding.backupCodeView, binding.backupCodeVisibilityToggle) + } + } + + private fun validateCode() { + enableContinueButton(binding.backupCodeView.codeValue.length == BACKUP_CODE_LENGTH) + } + + private fun handleForgot() { + val email = ConnectUserDatabaseUtil.getUser(requireContext())?.email + if (email != null) { + findNavController().navigate( + PersonalIdProfileBackupCodeFragmentDirections + .actionProfileBackupCodeToEmailVerification( + email = email, + workflow = EmailWorkFlow.RECOVERY, + emailOtpRequestCount = 0, + ), + ) + } else { + Toast + .makeText( + requireContext(), + R.string.personalid_no_email_forgot_backup_code_toast, + Toast.LENGTH_LONG, + ).show() + findNavController().popBackStack() + } + } + + override fun handleBackupCodeSubmission() { + val enteredCode = binding.backupCodeView.codeValue + val storedBackupCode = ConnectUserDatabaseUtil.getUser(requireContext())?.pin + if (enteredCode == storedBackupCode) { + findNavController().navigate(R.id.action_profile_backup_code_to_set_new_backup_code) + } else { + showError(getString(R.string.connect_backup_fail_title)) + } + } + + companion object { + private const val MAX_ATTEMPTS = 3 + } + + override fun navigateToMessageDisplay( + title: String, + message: String?, + isCancellable: Boolean, + phase: Int, + buttonText: Int, + ) { + // unreachable + } +} diff --git a/app/src/org/commcare/personalId/profile/PersonalIdProfileFragment.kt b/app/src/org/commcare/personalId/profile/PersonalIdProfileFragment.kt index d7a9e74948..64394ded54 100644 --- a/app/src/org/commcare/personalId/profile/PersonalIdProfileFragment.kt +++ b/app/src/org/commcare/personalId/profile/PersonalIdProfileFragment.kt @@ -41,6 +41,9 @@ class PersonalIdProfileFragment : BasePersonalIdProfileFragment() { viewModel = ViewModelProvider(this)[PersonalIdProfileViewModel::class.java] viewModel.profileDisplayModel.observe(viewLifecycleOwner) { displayProfileDetails(it) } binding.profileBtnForgetPersonalid.setOnClickListener { showForgetPersonalIdDialog() } + binding.profileChangeBackupCode.setOnClickListener { + findNavController().navigate(R.id.action_profile_to_profile_backup_code) + } } override fun onResume() { diff --git a/app/src/org/commcare/personalId/profile/SetNewBackupCodeFragment.kt b/app/src/org/commcare/personalId/profile/SetNewBackupCodeFragment.kt new file mode 100644 index 0000000000..a53b1a9ee3 --- /dev/null +++ b/app/src/org/commcare/personalId/profile/SetNewBackupCodeFragment.kt @@ -0,0 +1,16 @@ +package org.commcare.personalId.profile + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.fragment.app.Fragment + +class SetNewBackupCodeFragment : Fragment() { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View = TextView(requireContext()).apply { text = "Set New Backup Code — TODO" } +} diff --git a/app/unit-tests/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragmentTest.kt b/app/unit-tests/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragmentTest.kt new file mode 100644 index 0000000000..cce89295c3 --- /dev/null +++ b/app/unit-tests/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragmentTest.kt @@ -0,0 +1,143 @@ +package org.commcare.personalId.profile + +import android.view.View +import android.widget.TextView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.android.material.button.MaterialButton +import org.commcare.CommCareTestApplication +import org.commcare.dalvik.R +import org.commcare.views.connect.NumericCodeView +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowLooper + +@Config(application = CommCareTestApplication::class) +@RunWith(AndroidJUnit4::class) +class PersonalIdProfileBackupCodeFragmentTest : BasePersonalIdProfileTest() { + @Before + fun navigateToBackupCodeScreen() { + user.pin = "123456" // override to 6 digits so the code view can hold it + onUiThread { + navController.navigate(R.id.action_profile_to_profile_backup_code) + } + } + + private fun fragment() = + navHostFragment.childFragmentManager + .primaryNavigationFragment as PersonalIdProfileBackupCodeFragment + + private fun backupCodeView(): NumericCodeView = fragment().requireView().findViewById(R.id.backup_code_view) + + private fun confirmCodeLayout(): View = fragment().requireView().findViewById(R.id.confirm_code_layout) + + private fun confirmCodeLabel(): View = fragment().requireView().findViewById(R.id.confirm_code_label) + + private fun welcomeBackLayout(): View = fragment().requireView().findViewById(R.id.welcome_back_layout) + + private fun continueButton(): MaterialButton = fragment().requireView().findViewById(R.id.connect_backup_code_button) + + private fun errorMessage(): TextView = fragment().requireView().findViewById(R.id.connect_backup_code_error_message) + + private fun forgotButton(): TextView = fragment().requireView().findViewById(R.id.not_me_button) + + // ===== Initial state ===== + + @Test + fun `confirm code layout is hidden`() { + assertEquals(View.GONE, confirmCodeLabel().visibility) + assertEquals(View.GONE, confirmCodeLayout().visibility) + } + + @Test + fun `welcome back layout is hidden`() { + assertEquals(View.GONE, welcomeBackLayout().visibility) + } + + @Test + fun `continue button starts disabled`() { + assertFalse(continueButton().isEnabled) + } + + @Test + fun `error message starts hidden`() { + assertEquals(View.GONE, errorMessage().visibility) + } + + @Test + fun `forgot backup code link is visible`() { + assertEquals(View.VISIBLE, forgotButton().visibility) + } + + // ===== Validation ===== + + @Test + fun `continue button enables when 6 digits are entered`() { + onUiThread { backupCodeView().setCode("000000") } + ShadowLooper.idleMainLooper() + + assertTrue(continueButton().isEnabled) + } + + @Test + fun `continue button stays disabled until 6 digits`() { + onUiThread { backupCodeView().setCode("12345") } + ShadowLooper.idleMainLooper() + + assertFalse(continueButton().isEnabled) + } + + // ===== Correct code ===== + + @Test + fun `correct code navigates to set-new-backup-code`() { + onUiThread { backupCodeView().setCode("123456") } + ShadowLooper.idleMainLooper() + onUiThread { continueButton().performClick() } + + assertEquals(R.id.personalid_set_new_backup_code_fragment, currentDestinationId()) + } + + // ===== Wrong code ===== + + @Test + fun `wrong code shows error and stays on screen`() { + onUiThread { backupCodeView().setCode("000000") } + ShadowLooper.idleMainLooper() + onUiThread { continueButton().performClick() } + + assertEquals(View.VISIBLE, errorMessage().visibility) + assertEquals(R.id.personalid_profile_backup_code_fragment, currentDestinationId()) + } + + @Test + fun `continue button re-enables after wrong code`() { + onUiThread { backupCodeView().setCode("000000") } + ShadowLooper.idleMainLooper() + onUiThread { continueButton().performClick() } + + assertTrue(continueButton().isEnabled) + } + + // ===== Forgot ===== + + @Test + fun `forgot with email navigates to email verification`() { + // user.email is "ada@example.com" per BasePersonalIdProfileTest + onUiThread { forgotButton().performClick() } + + assertEquals(R.id.personalid_email_verification_fragment, currentDestinationId()) + } + + @Test + fun `forgot with no email pops back to profile`() { + user.email = null + onUiThread { forgotButton().performClick() } + + assertEquals(R.id.personalid_profile_fragment, currentDestinationId()) + } +} diff --git a/app/unit-tests/src/org/commcare/personalId/profile/PersonalIdProfileFragmentTest.kt b/app/unit-tests/src/org/commcare/personalId/profile/PersonalIdProfileFragmentTest.kt index c71453994c..0d840580eb 100644 --- a/app/unit-tests/src/org/commcare/personalId/profile/PersonalIdProfileFragmentTest.kt +++ b/app/unit-tests/src/org/commcare/personalId/profile/PersonalIdProfileFragmentTest.kt @@ -71,4 +71,14 @@ class PersonalIdProfileFragmentTest : BasePersonalIdProfileTest() { assertNotNull("Forget PersonalID should show a confirmation dialog", dialog) assertTrue("The confirmation dialog should be visible", dialog.isShowing) } + + @Test + fun `tapping change backup code row navigates to backup code screen`() { + val changeBackupCodeRow = + fragment().requireView().findViewById(R.id.profile_change_backup_code) + + onUiThread { changeBackupCodeRow.performClick() } + + assertEquals(R.id.personalid_profile_backup_code_fragment, currentDestinationId()) + } } From 1354bdf3a88fb2dd44da47c5ff43adbd5a9d7c1d Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 25 Aug 2026 13:25:12 +0530 Subject: [PATCH 03/10] feat(ccct-2709): lock verify screen after 3 failed backup code attempts --- app/res/layout/fragment_recovery_code.xml | 2 +- app/res/values-es/strings.xml | 1 + app/res/values-fr/strings.xml | 1 + app/res/values-ha/strings.xml | 1 + app/res/values-hi/strings.xml | 1 + app/res/values-lt/strings.xml | 1 + app/res/values-no/strings.xml | 1 + app/res/values-pt/strings.xml | 1 + app/res/values-sw/strings.xml | 1 + app/res/values-ti/strings.xml | 1 + app/res/values/strings.xml | 1 + .../personalId/PersonalIdUserPreferences.kt | 43 +++++++++ .../PersonalIdProfileBackupCodeFragment.kt | 46 ++++++++-- ...PersonalIdProfileBackupCodeFragmentTest.kt | 88 +++++++++++++++++-- 14 files changed, 171 insertions(+), 18 deletions(-) diff --git a/app/res/layout/fragment_recovery_code.xml b/app/res/layout/fragment_recovery_code.xml index cebc632874..cf6bc4a036 100644 --- a/app/res/layout/fragment_recovery_code.xml +++ b/app/res/layout/fragment_recovery_code.xml @@ -223,7 +223,7 @@ android:layout_height="wrap_content" android:text="@string/personalid_this_is_not_me" android:textColor="@color/cc_brand_color" - android:textSize="12sp" + android:textSize="14sp" android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:layout_marginEnd="16dp" diff --git a/app/res/values-es/strings.xml b/app/res/values-es/strings.xml index 20295a25cb..fb8386be8c 100644 --- a/app/res/values-es/strings.xml +++ b/app/res/values-es/strings.xml @@ -417,6 +417,7 @@ Tu perfil se ha actualizado correctamente. Cambiar código de respaldo ¿Olvidaste tu código de respaldo? + Demasiados intentos. Por favor, inténtalo de nuevo más tarde. Por favor, añade una dirección de correo electrónico Establecer nuevo código de respaldo Detalles de la entrega diff --git a/app/res/values-fr/strings.xml b/app/res/values-fr/strings.xml index 86f45ef2d4..bcf5217194 100644 --- a/app/res/values-fr/strings.xml +++ b/app/res/values-fr/strings.xml @@ -414,6 +414,7 @@ License. Votre profil a été mis à jour avec succès. Changer le code de sauvegarde Vous avez oublié votre code de sauvegarde ? + Trop de tentatives. Veuillez réessayer plus tard. Veuillez ajouter une adresse e-mail Définir un nouveau code de sauvegarde Détails de livraison diff --git a/app/res/values-ha/strings.xml b/app/res/values-ha/strings.xml index 12c29a37f6..b4b820237e 100644 --- a/app/res/values-ha/strings.xml +++ b/app/res/values-ha/strings.xml @@ -252,6 +252,7 @@ Don cikakken aiki na wurin zama na aikace-aikace da yawa, da fatan za a yi sabon An sabunta bayanan martabarka cikin nasara. Canza lambar sirrin ajiyar bayanai Ka manta da lambar sirrin ajiyar bayananka? + Yunkuri da yawa. Da fatan za a sake gwadawa daga baya. Da fatan za a ƙara adireshin imel Saita sabuwar lambar sirrin ajiyar bayanai Cikakkun Bayanan Isarwa diff --git a/app/res/values-hi/strings.xml b/app/res/values-hi/strings.xml index 9fa0fe2c7d..7ec52b5448 100644 --- a/app/res/values-hi/strings.xml +++ b/app/res/values-hi/strings.xml @@ -414,6 +414,7 @@ License. आपकी प्रोफ़ाइल सफलतापूर्वक अपडेट कर दी गई है। बैकअप कोड बदलें अपना बैकअप कोड भूल गए? + Too many attempts. Please try again later. Please add an email address वितरण विवरण वितरण विवरण की समीक्षा करें diff --git a/app/res/values-lt/strings.xml b/app/res/values-lt/strings.xml index efeaf43207..d9a509d57d 100644 --- a/app/res/values-lt/strings.xml +++ b/app/res/values-lt/strings.xml @@ -105,6 +105,7 @@ Jūsų profilis sėkmingai atnaujintas. Keisti atsarginį kodą Pamiršote savo atsarginį kodą? + Too many attempts. Please try again later. Please add an email address Prisijungti / Registruotis Pagalba diff --git a/app/res/values-no/strings.xml b/app/res/values-no/strings.xml index e5b1669bb6..8b197239dd 100644 --- a/app/res/values-no/strings.xml +++ b/app/res/values-no/strings.xml @@ -105,6 +105,7 @@ Profilen din er oppdatert. Endre sikkerhetskode Glemt sikkerhetskoden din? + Too many attempts. Please try again later. Please add an email address Logg inn / Registrer deg Hjelp diff --git a/app/res/values-pt/strings.xml b/app/res/values-pt/strings.xml index c9b268f606..cac77e2950 100644 --- a/app/res/values-pt/strings.xml +++ b/app/res/values-pt/strings.xml @@ -430,6 +430,7 @@ Seu perfil foi atualizado com sucesso. Alterar código de backup Esqueceu o seu código de backup? + Muitas tentativas. Por favor, tente novamente mais tarde. Por favor, adicione um endereço de e-mail Definir novo código de backup Detalhes da entrega diff --git a/app/res/values-sw/strings.xml b/app/res/values-sw/strings.xml index ce8dcbeed9..befced4c72 100644 --- a/app/res/values-sw/strings.xml +++ b/app/res/values-sw/strings.xml @@ -416,6 +416,7 @@ Wasifu wako umesasishwa kwa mafanikio. Badilisha nambari ya hifadhi nakala Umesahau nambari yako ya hifadhi nakala? + Too many attempts. Please try again later. Please add an email address Maelezo ya Uwasilishaji Kagua maelezo ya utoaji diff --git a/app/res/values-ti/strings.xml b/app/res/values-ti/strings.xml index 2e1fec5a20..fd3fc240b7 100644 --- a/app/res/values-ti/strings.xml +++ b/app/res/values-ti/strings.xml @@ -417,6 +417,7 @@ ፕሮፋይልኩም ብዓወት ተሐዲሱ። ናይ ምትካእ ኮድ ቀይር ናይ ምትካእ ኮድካ ረሲዕካ? + ብዙሕ ፈተናታት። በጃኻ ድሕሪ ሕጂ እንደገና ፈትን። በጃኻ ናይ ኢ-መይል ኣድራሻ ወስኽ ሓድሽ ናይ ምትካእ ኮድ ኣቐምጥ ዝርዝር (ኣተገባብራ)ኣወሃህባ diff --git a/app/res/values/strings.xml b/app/res/values/strings.xml index 6be125f0be..29e0948b28 100644 --- a/app/res/values/strings.xml +++ b/app/res/values/strings.xml @@ -800,6 +800,7 @@ This is not me Change backup code Forgot your backup code? + Too many attempts. Please try again later. Please add an email address Welcome back %s You have entered the wrong Backup Code. Please try again. Your account will be locked after %d more incorrect attempts. diff --git a/app/src/org/commcare/personalId/PersonalIdUserPreferences.kt b/app/src/org/commcare/personalId/PersonalIdUserPreferences.kt index a21f45b500..39862db9e5 100644 --- a/app/src/org/commcare/personalId/PersonalIdUserPreferences.kt +++ b/app/src/org/commcare/personalId/PersonalIdUserPreferences.kt @@ -10,9 +10,52 @@ object PersonalIdUserPreferences { private const val PREFS_NAME = "personalid_prefs" private const val KEY_EMAIL_OFFER_COUNT = "email_offer_count" private const val KEY_LAST_EMAIL_OFFER_DATE = "last_email_offer_date" + private const val KEY_BACKUP_CODE_FAILED_ATTEMPTS = "backup_code_failed_attempts" + private const val KEY_BACKUP_CODE_LOCKOUT_START = "backup_code_lockout_start" + private const val KEY_BACKUP_CODE_WINDOW_START = "backup_code_window_start" + private const val BACKUP_CODE_LOCKOUT_DURATION_MS = 24 * 60 * 60 * 1000L private fun prefs(): SharedPreferences = CommCareApplication.instance().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + /** + * Records a backup-code failure and returns the new failure count within the current 24-hour + * window. If the previous failure window has expired, the count resets before incrementing. + */ + fun recordBackupCodeFailure(): Int { + val now = System.currentTimeMillis() + val windowStart = prefs().getLong(KEY_BACKUP_CODE_WINDOW_START, -1L) + val baseCount = + if (windowStart == -1L || now - windowStart >= BACKUP_CODE_LOCKOUT_DURATION_MS) { + prefs().edit { + putLong(KEY_BACKUP_CODE_WINDOW_START, now) + remove(KEY_BACKUP_CODE_LOCKOUT_START) + } + 0 + } else { + prefs().getInt(KEY_BACKUP_CODE_FAILED_ATTEMPTS, 0) + } + val next = baseCount + 1 + prefs().edit { putInt(KEY_BACKUP_CODE_FAILED_ATTEMPTS, next) } + return next + } + + fun triggerBackupCodeLockout() { + prefs().edit { putLong(KEY_BACKUP_CODE_LOCKOUT_START, System.currentTimeMillis()) } + } + + fun isBackupCodeLockedOut(): Boolean { + val start = prefs().getLong(KEY_BACKUP_CODE_LOCKOUT_START, -1L) + return start != -1L && System.currentTimeMillis() - start < BACKUP_CODE_LOCKOUT_DURATION_MS + } + + fun clearBackupCodeLockout() { + prefs().edit { + remove(KEY_BACKUP_CODE_FAILED_ATTEMPTS) + remove(KEY_BACKUP_CODE_LOCKOUT_START) + remove(KEY_BACKUP_CODE_WINDOW_START) + } + } + /** * Email Offer Count (Int) — 0 = never offered, 1 = first offer shown, 2 = both offers shown */ diff --git a/app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt b/app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt index c8c68250ae..676ea07030 100644 --- a/app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt +++ b/app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt @@ -10,8 +10,22 @@ import org.commcare.connect.database.ConnectUserDatabaseUtil import org.commcare.dalvik.R import org.commcare.fragments.personalId.BasePersonalIdBackupCodeFragment import org.commcare.fragments.personalId.EmailWorkFlow +import org.commcare.personalId.PersonalIdUserPreferences class PersonalIdProfileBackupCodeFragment : BasePersonalIdBackupCodeFragment() { + private val forgotBackupCodeButton get() = binding.notMeButton + private var isLocked = false + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { + super.onViewCreated(view, savedInstanceState) + if (PersonalIdUserPreferences.isBackupCodeLockedOut()) { + enterLockedState() + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -30,8 +44,8 @@ class PersonalIdProfileBackupCodeFragment : BasePersonalIdBackupCodeFragment() { binding.confirmCodeLayout.visibility = View.GONE binding.confirmCodeLabel.visibility = View.GONE binding.welcomeBackLayout.visibility = View.GONE - binding.notMeButton.visibility = View.VISIBLE - binding.notMeButton.setText(R.string.personalid_forgot_backup_code) + forgotBackupCodeButton.visibility = View.VISIBLE + forgotBackupCodeButton.setText(R.string.personalid_forgot_backup_code) enableContinueButton(false) setupListeners() } @@ -40,14 +54,16 @@ class PersonalIdProfileBackupCodeFragment : BasePersonalIdBackupCodeFragment() { binding.backupCodeView.setOnCodeChangedListener { validateCode() } binding.backupCodeView.setOnEnterKeyPressedListener { submitIfEnabled() } binding.connectBackupCodeButton.setOnClickListener { handleBackupCodeSubmission() } - binding.notMeButton.setOnClickListener { handleForgot() } + forgotBackupCodeButton.setOnClickListener { handleForgot() } binding.backupCodeVisibilityToggle.setOnClickListener { togglePasswordVisibility(binding.backupCodeView, binding.backupCodeVisibilityToggle) } } private fun validateCode() { - enableContinueButton(binding.backupCodeView.codeValue.length == BACKUP_CODE_LENGTH) + if (!isLocked) { + enableContinueButton(binding.backupCodeView.codeValue.length == BACKUP_CODE_LENGTH) + } } private fun handleForgot() { @@ -56,9 +72,9 @@ class PersonalIdProfileBackupCodeFragment : BasePersonalIdBackupCodeFragment() { findNavController().navigate( PersonalIdProfileBackupCodeFragmentDirections .actionProfileBackupCodeToEmailVerification( - email = email, - workflow = EmailWorkFlow.RECOVERY, - emailOtpRequestCount = 0, + email, + EmailWorkFlow.RECOVERY, + 0, ), ) } else { @@ -76,12 +92,26 @@ class PersonalIdProfileBackupCodeFragment : BasePersonalIdBackupCodeFragment() { val enteredCode = binding.backupCodeView.codeValue val storedBackupCode = ConnectUserDatabaseUtil.getUser(requireContext())?.pin if (enteredCode == storedBackupCode) { + PersonalIdUserPreferences.clearBackupCodeLockout() findNavController().navigate(R.id.action_profile_backup_code_to_set_new_backup_code) } else { - showError(getString(R.string.connect_backup_fail_title)) + val attempts = PersonalIdUserPreferences.recordBackupCodeFailure() + if (attempts >= MAX_ATTEMPTS) { + PersonalIdUserPreferences.triggerBackupCodeLockout() + enterLockedState() + } else { + showError(getString(R.string.connect_backup_fail_title)) + } } } + private fun enterLockedState() { + isLocked = true + showError(getString(R.string.personalid_backup_code_too_many_attempts)) + binding.backupCodeView.isEnabled = false + enableContinueButton(false) + } + companion object { private const val MAX_ATTEMPTS = 3 } diff --git a/app/unit-tests/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragmentTest.kt b/app/unit-tests/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragmentTest.kt index cce89295c3..c1c1cdcefa 100644 --- a/app/unit-tests/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragmentTest.kt +++ b/app/unit-tests/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragmentTest.kt @@ -1,11 +1,13 @@ package org.commcare.personalId.profile +import android.content.Context import android.view.View import android.widget.TextView import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.android.material.button.MaterialButton import org.commcare.CommCareTestApplication import org.commcare.dalvik.R +import org.commcare.personalId.PersonalIdUserPreferences import org.commcare.views.connect.NumericCodeView import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -15,12 +17,14 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.shadows.ShadowLooper +import org.robolectric.shadows.ShadowToast @Config(application = CommCareTestApplication::class) @RunWith(AndroidJUnit4::class) class PersonalIdProfileBackupCodeFragmentTest : BasePersonalIdProfileTest() { @Before fun navigateToBackupCodeScreen() { + PersonalIdUserPreferences.clearBackupCodeLockout() user.pin = "123456" // override to 6 digits so the code view can hold it onUiThread { navController.navigate(R.id.action_profile_to_profile_backup_code) @@ -45,6 +49,11 @@ class PersonalIdProfileBackupCodeFragmentTest : BasePersonalIdProfileTest() { private fun forgotButton(): TextView = fragment().requireView().findViewById(R.id.not_me_button) + private fun setCodeAndContinue(code: String = "000000") { + onUiThread { backupCodeView().setCode(code) } + onUiThread { continueButton().performClick() } + } + // ===== Initial state ===== @Test @@ -95,9 +104,7 @@ class PersonalIdProfileBackupCodeFragmentTest : BasePersonalIdProfileTest() { @Test fun `correct code navigates to set-new-backup-code`() { - onUiThread { backupCodeView().setCode("123456") } - ShadowLooper.idleMainLooper() - onUiThread { continueButton().performClick() } + setCodeAndContinue("123456") assertEquals(R.id.personalid_set_new_backup_code_fragment, currentDestinationId()) } @@ -106,20 +113,79 @@ class PersonalIdProfileBackupCodeFragmentTest : BasePersonalIdProfileTest() { @Test fun `wrong code shows error and stays on screen`() { - onUiThread { backupCodeView().setCode("000000") } - ShadowLooper.idleMainLooper() - onUiThread { continueButton().performClick() } - + setCodeAndContinue() assertEquals(View.VISIBLE, errorMessage().visibility) assertEquals(R.id.personalid_profile_backup_code_fragment, currentDestinationId()) } @Test fun `continue button re-enables after wrong code`() { - onUiThread { backupCodeView().setCode("000000") } + setCodeAndContinue() + assertTrue(continueButton().isEnabled) + } + + // ===== Locked state ===== + + @Test + fun `wrong code three times enters locked state`() { + setCodeAndContinue() + setCodeAndContinue() + setCodeAndContinue() + + assertEquals(View.VISIBLE, forgotButton().visibility) + assertFalse(backupCodeView().isEnabled) + assertFalse(continueButton().isEnabled) + assertEquals(View.VISIBLE, errorMessage().visibility) + assertTrue(PersonalIdUserPreferences.isBackupCodeLockedOut()) + } + + @Test + fun `continue button stays disabled in locked state even if code is set`() { + setCodeAndContinue() + setCodeAndContinue() + setCodeAndContinue() + + onUiThread { backupCodeView().setCode("123456") } + + assertFalse(continueButton().isEnabled) + } + + @Test + fun `lockout in prefs causes locked state on fragment init`() { + PersonalIdUserPreferences.triggerBackupCodeLockout() + + // Navigate back to profile, then forward again so the fragment is recreated + onUiThread { navController.popBackStack() } ShadowLooper.idleMainLooper() - onUiThread { continueButton().performClick() } + onUiThread { navController.navigate(R.id.action_profile_to_profile_backup_code) } + ShadowLooper.idleMainLooper() + + assertFalse(backupCodeView().isEnabled) + assertFalse(continueButton().isEnabled) + assertEquals(View.VISIBLE, errorMessage().visibility) + assertEquals(View.VISIBLE, forgotButton().visibility) + } + @Test + fun `third wrong attempt after 24 hours does not trigger lockout`() { + // Two wrong attempts within the window + setCodeAndContinue() + onUiThread { backupCodeView().clearCode() } + setCodeAndContinue() + onUiThread { backupCodeView().clearCode() } + + // Backdated the window start to more than 24 hours ago so the next failure resets the count + activity + .getSharedPreferences("personalid_prefs", Context.MODE_PRIVATE) + .edit() + .putLong("backup_code_window_start", System.currentTimeMillis() - 25 * 60 * 60 * 1000L) + .commit() + + // Third wrong attempt + setCodeAndContinue() + + assertFalse(PersonalIdUserPreferences.isBackupCodeLockedOut()) + assertEquals(View.VISIBLE, errorMessage().visibility) assertTrue(continueButton().isEnabled) } @@ -138,6 +204,10 @@ class PersonalIdProfileBackupCodeFragmentTest : BasePersonalIdProfileTest() { user.email = null onUiThread { forgotButton().performClick() } + assertEquals( + activity.getString(R.string.personalid_no_email_forgot_backup_code_toast), + ShadowToast.getTextOfLatestToast(), + ) assertEquals(R.id.personalid_profile_fragment, currentDestinationId()) } } From 1f8d79086f134c33d25a4aa0b610128b0735b7e9 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 26 Aug 2026 17:30:01 +0530 Subject: [PATCH 04/10] feat(ccct-2709): grey fill and disable child fields in NumericCodeView when disabled Co-Authored-By: Claude Sonnet 4.6 --- .../org/commcare/views/connect/NumericCodeView.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/src/org/commcare/views/connect/NumericCodeView.java b/app/src/org/commcare/views/connect/NumericCodeView.java index 395912cb25..239af85038 100644 --- a/app/src/org/commcare/views/connect/NumericCodeView.java +++ b/app/src/org/commcare/views/connect/NumericCodeView.java @@ -289,6 +289,9 @@ private GradientDrawable createBackgroundDrawable() { GradientDrawable drawable = new GradientDrawable(); drawable.setCornerRadius(borderRadius); drawable.setStroke(borderWidth, isErrorState ? errorBorderColor : borderColor); + if (!isEnabled()) { + drawable.setColor(Color.LTGRAY); + } return drawable; } @@ -382,6 +385,15 @@ public void setOnEnterKeyPressedListener(OnEnterKeyPressedListener listener) { this.enterKeyPressedListener = listener; } + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + for (int i = 0; i < getChildCount(); i++) { + getChildAt(i).setEnabled(enabled); + } + updateUi(); + } + public void setCode(String code) { if (code.length() > digitCount) { throw new IllegalArgumentException("Code length exceeds the digit count"); From 44b40603404ec616c5ef48c890047e2a1a8a7988 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 26 Aug 2026 20:52:38 +0530 Subject: [PATCH 05/10] port backupcode fix from 777edf19d037a0af82bf1300b44b4bc7983ef317 --- .../fragments/personalId/PersonalIdBackupCodeFragment.kt | 2 +- .../personalId/PersonalIdBackupCodeFragmentRecoveryTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.kt b/app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.kt index 86739edd72..ce6b30c888 100644 --- a/app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.kt +++ b/app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.kt @@ -134,10 +134,10 @@ class PersonalIdBackupCodeFragment : BasePersonalIdBackupCodeFragment() { null, PersonalIdWorkflow.CONFIGURATION, ) + personalIdSessionData.backupCode = binding.backupCodeView.codeValue if (isRecovery) { confirmBackupCode() } else { - personalIdSessionData.backupCode = binding.backupCodeView.codeValue if (ReleaseToggleHelper.isEmailOtpVerificationActive(personalIdSessionData)) { navigateToEmail() } else { diff --git a/app/unit-tests/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragmentRecoveryTest.kt b/app/unit-tests/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragmentRecoveryTest.kt index 69c570bb56..5a442543d5 100644 --- a/app/unit-tests/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragmentRecoveryTest.kt +++ b/app/unit-tests/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragmentRecoveryTest.kt @@ -164,7 +164,7 @@ class PersonalIdBackupCodeFragmentRecoveryTest : BasePersonalIdBackupCodeFragmen assertEquals(TEST_PHOTO_BASE64, storedUser.photo) assertEquals(PersonalIdSessionData.PIN, storedUser.requiredLock) // Recovery never writes the entered code onto the session data, so the stored record has no pin. - assertNull(storedUser.pin) + assertEquals(TEST_BACKUP_CODE, storedUser.pin) assertMessageDisplay( title = fragment.getString(R.string.connect_recovery_success_title), From 6d2c7c8612e6aecaecbefde9eaf970af11249c81 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 28 Aug 2026 14:44:07 +0530 Subject: [PATCH 06/10] refactor(ccct-2710): abstract backup code functionality into base classes Extract shared validation, listener setup, and visibility toggle logic from PersonalIdBackupCodeFragment and PersonalIdProfileBackupCodeFragment into BasePersonalIdBackupCodeFragment. Introduce validateBackupCodeInput(), validateBackupCodeAndEnableContinue(), setupListeners(), and an onCodeChanged() hook so subclasses can add guards (e.g. isLocked check) without re-wiring listeners. Co-Authored-By: Claude Sonnet 4.6 --- .../BasePersonalIdBackupCodeFragment.kt | 53 ++++++++++++++++++- .../PersonalIdBackupCodeFragment.kt | 45 ++-------------- .../PersonalIdProfileBackupCodeFragment.kt | 27 ++-------- 3 files changed, 61 insertions(+), 64 deletions(-) diff --git a/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt b/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt index 84f11e893f..04b3946126 100644 --- a/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt +++ b/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt @@ -32,7 +32,7 @@ abstract class BasePersonalIdBackupCodeFragment : BasePersonalIdFragment() { binding.confirmCodeView.clearCode() } - protected open fun submitIfEnabled() { + protected open fun submitIfEnabled() { if (binding.connectBackupCodeButton.isEnabled) handleBackupCodeSubmission() } @@ -62,6 +62,57 @@ abstract class BasePersonalIdBackupCodeFragment : BasePersonalIdFragment() { binding.connectBackupCodeButton.isEnabled = enabled } + protected open fun onCodeChanged() { + validateBackupCodeAndEnableContinue() + } + + protected open fun setupListeners() { + binding.backupCodeView.setOnCodeChangedListener { onCodeChanged() } + binding.backupCodeView.setOnEnterKeyPressedListener { submitIfEnabled() } + binding.confirmCodeView.setOnCodeChangedListener { onCodeChanged() } + binding.confirmCodeView.setOnEnterKeyPressedListener { submitIfEnabled() } + binding.connectBackupCodeButton.setOnClickListener { handleBackupCodeSubmission() } + binding.backupCodeVisibilityToggle.setOnClickListener { + togglePasswordVisibility(binding.backupCodeView, binding.backupCodeVisibilityToggle) + } + binding.confirmCodeVisibilityToggle.setOnClickListener { + togglePasswordVisibility(binding.confirmCodeView, binding.confirmCodeVisibilityToggle) + } + } + + protected fun validateBackupCodeAndEnableContinue() { + enableContinueButton(validateBackupCodeInput()) + } + + protected fun validateBackupCodeInput(): Boolean { + val backupCode = binding.backupCodeView.codeValue + val isBackupCodeComplete = backupCode.length == BACKUP_CODE_LENGTH + if (binding.confirmCodeLayout.visibility != View.VISIBLE) { + enableContinueButton(isBackupCodeComplete) + return true + } + val confirmCode = binding.confirmCodeView.codeValue + val isConfirmCodeComplete = confirmCode.length == BACKUP_CODE_LENGTH + if (isBackupCodeComplete && isConfirmCodeComplete && backupCode != confirmCode) { + showError(getString(R.string.connect_backup_code_mismatch)) + } else { + clearError() + } + val isValid = isBackupCodeComplete && backupCode == confirmCode + enableContinueButton(isValid) + return isValid + } + + override fun navigateToMessageDisplay( + title: String, + message: String?, + isCancellable: Boolean, + phase: Int, + buttonText: Int, + ) { + // no default implementation + } + companion object { const val BACKUP_CODE_LENGTH = 6 } diff --git a/app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.kt b/app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.kt index ce6b30c888..206da13867 100644 --- a/app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.kt +++ b/app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.kt @@ -21,7 +21,6 @@ import org.commcare.google.services.analytics.FirebaseAnalyticsUtil import org.commcare.personalId.PersonalIdRecoveryCompleter import org.commcare.personalId.PersonalIdUserPreferences import org.commcare.utils.MediaUtil -import org.commcare.views.connect.NumericCodeView import java.util.Date class PersonalIdBackupCodeFragment : BasePersonalIdBackupCodeFragment() { @@ -48,7 +47,7 @@ class PersonalIdBackupCodeFragment : BasePersonalIdBackupCodeFragment() { override fun onResume() { super.onResume() - validateBackupCodeInputs() + validateBackupCodeAndEnableContinue() } private fun configureUiByMode() { @@ -79,47 +78,11 @@ class PersonalIdBackupCodeFragment : BasePersonalIdBackupCodeFragment() { } } - private fun setupListeners() { - val codeChangedListener = NumericCodeView.OnCodeChangedListener { validateBackupCodeInputs() } - - binding.backupCodeView.setOnCodeChangedListener(codeChangedListener) - binding.confirmCodeView.setOnCodeChangedListener(codeChangedListener) - - binding.backupCodeView.setCodeCompleteListener { - if (isRecovery) { - submitIfEnabled() - } - } - + override fun setupListeners() { + super.setupListeners() + binding.backupCodeView.setCodeCompleteListener { if (isRecovery) submitIfEnabled() } binding.confirmCodeView.setCodeCompleteListener { submitIfEnabled() } - - val enterKeyListener = NumericCodeView.OnEnterKeyPressedListener { submitIfEnabled() } - binding.backupCodeView.setOnEnterKeyPressedListener(enterKeyListener) - binding.confirmCodeView.setOnEnterKeyPressedListener(enterKeyListener) - - binding.connectBackupCodeButton.setOnClickListener { handleBackupCodeSubmission() } binding.notMeButton.setOnClickListener { handleNotMeButtonPressed() } - - binding.backupCodeVisibilityToggle.setOnClickListener { - togglePasswordVisibility(binding.backupCodeView, binding.backupCodeVisibilityToggle) - } - binding.confirmCodeVisibilityToggle.setOnClickListener { - togglePasswordVisibility(binding.confirmCodeView, binding.confirmCodeVisibilityToggle) - } - } - - private fun validateBackupCodeInputs() { - val backupCode = binding.backupCodeView.codeValue - val confirmCode = binding.confirmCodeView.codeValue - - val isCodeComplete = backupCode.length == BACKUP_CODE_LENGTH - val isCodeConfirmed = isRecovery || backupCode == confirmCode - val showMismatch = isCodeComplete && !isCodeConfirmed && confirmCode.length == BACKUP_CODE_LENGTH - val errorText = if (showMismatch) getString(R.string.connect_backup_code_mismatch) else "" - - binding.connectBackupCodeErrorMessage.visibility = if (showMismatch) View.VISIBLE else View.GONE - binding.connectBackupCodeErrorMessage.text = errorText - enableContinueButton(isCodeComplete && isCodeConfirmed) } private fun handleNotMeButtonPressed() { diff --git a/app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt b/app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt index 676ea07030..c44b176f56 100644 --- a/app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt +++ b/app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt @@ -50,20 +50,13 @@ class PersonalIdProfileBackupCodeFragment : BasePersonalIdBackupCodeFragment() { setupListeners() } - private fun setupListeners() { - binding.backupCodeView.setOnCodeChangedListener { validateCode() } - binding.backupCodeView.setOnEnterKeyPressedListener { submitIfEnabled() } - binding.connectBackupCodeButton.setOnClickListener { handleBackupCodeSubmission() } - forgotBackupCodeButton.setOnClickListener { handleForgot() } - binding.backupCodeVisibilityToggle.setOnClickListener { - togglePasswordVisibility(binding.backupCodeView, binding.backupCodeVisibilityToggle) - } + override fun onCodeChanged() { + if (!isLocked) validateBackupCodeAndEnableContinue() } - private fun validateCode() { - if (!isLocked) { - enableContinueButton(binding.backupCodeView.codeValue.length == BACKUP_CODE_LENGTH) - } + override fun setupListeners() { + super.setupListeners() + forgotBackupCodeButton.setOnClickListener { handleForgot() } } private fun handleForgot() { @@ -115,14 +108,4 @@ class PersonalIdProfileBackupCodeFragment : BasePersonalIdBackupCodeFragment() { companion object { private const val MAX_ATTEMPTS = 3 } - - override fun navigateToMessageDisplay( - title: String, - message: String?, - isCancellable: Boolean, - phase: Int, - buttonText: Int, - ) { - // unreachable - } } From a161d31bb108a6ad99ce1be07abb508f4e4c8afb Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 31 Aug 2026 12:05:04 +0530 Subject: [PATCH 07/10] refactor(ccct-2710): abstract setUpView into BasePersonalIdBackupCodeFragment Co-Authored-By: Claude Sonnet 4.6 --- .../BasePersonalIdBackupCodeFragment.kt | 29 +++++++++++ .../PersonalIdBackupCodeFragment.kt | 49 +++++++------------ .../PersonalIdProfileBackupCodeFragment.kt | 29 +++-------- 3 files changed, 54 insertions(+), 53 deletions(-) diff --git a/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt b/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt index 04b3946126..8eed4e2534 100644 --- a/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt +++ b/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt @@ -18,10 +18,17 @@ abstract class BasePersonalIdBackupCodeFragment : BasePersonalIdFragment() { savedInstanceState: Bundle?, ): View { binding = FragmentRecoveryCodeBinding.inflate(inflater, container, false) + initData() + setUpView() + setupListeners() clearBackupCodeFields() return binding.root } + open fun initData() {} + + abstract fun setUpView() + override fun onResume() { super.onResume() binding.backupCodeView.requestFocus(requireActivity()) @@ -62,6 +69,28 @@ abstract class BasePersonalIdBackupCodeFragment : BasePersonalIdFragment() { binding.connectBackupCodeButton.isEnabled = enabled } + protected fun setUpInitialState( + titleResId: Int, + showConfirmCode: Boolean, + subtitle: CharSequence, + notMeButtonTextId : Int? = null, + ) { + requireActivity().title = getString(titleResId) + binding.recoveryCodeTilte.setText(titleResId) + binding.backupCodeLayout.visibility = View.VISIBLE + binding.welcomeBackLayout.visibility = View.GONE + binding.notMeButton.visibility = View.GONE + val confirmVisibility = if (showConfirmCode) View.VISIBLE else View.GONE + binding.confirmCodeLayout.visibility = confirmVisibility + binding.confirmCodeLabel.visibility = confirmVisibility + binding.backupCodeSubtitle.text = subtitle + notMeButtonTextId?.let { + binding.notMeButton.setText(getString(it)) + binding.notMeButton.visibility = View.VISIBLE + } + enableContinueButton(false) + } + protected open fun onCodeChanged() { validateBackupCodeAndEnableContinue() } diff --git a/app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.kt b/app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.kt index 206da13867..2afc12dcb5 100644 --- a/app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.kt +++ b/app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.kt @@ -4,7 +4,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.annotation.StringRes import androidx.lifecycle.ViewModelProvider import androidx.navigation.NavDirections import androidx.navigation.findNavController @@ -27,46 +26,34 @@ class PersonalIdBackupCodeFragment : BasePersonalIdBackupCodeFragment() { private lateinit var personalIdSessionData: PersonalIdSessionData private var isRecovery = false - @StringRes - private var titleId = 0 + override fun onResume() { + super.onResume() + validateBackupCodeAndEnableContinue() + } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View { - val view = super.onCreateView(inflater, container, savedInstanceState) + override fun initData() { personalIdSessionData = ViewModelProvider(requireActivity())[PersonalIdSessionDataViewModel::class.java] .personalIdSessionData - configureUiByMode() - setupListeners() - requireActivity().title = getString(titleId) - return view - } - - override fun onResume() { - super.onResume() - validateBackupCodeAndEnableContinue() } - private fun configureUiByMode() { + override fun setUpView() { isRecovery = personalIdSessionData.accountExists == true if (isRecovery) { - titleId = R.string.connect_backup_code_title_confirm + setUpInitialState( + titleResId = R.string.connect_backup_code_title_confirm, + showConfirmCode = false, + subtitle = getString(R.string.connect_backup_code_message), + ) binding.recoveryCodeTilte.setText(R.string.connect_backup_code_message_title) - binding.backupCodeSubtitle.setText(R.string.connect_backup_code_message) - binding.backupCodeLayout.visibility = View.VISIBLE - binding.confirmCodeLabel.visibility = View.GONE - binding.confirmCodeLayout.visibility = View.GONE + binding.welcomeBackLayout.visibility = View.VISIBLE setUserNameAndPhoto() } else { - titleId = R.string.connect_backup_code_title_set - binding.backupCodeSubtitle.text = getString(R.string.connect_backup_code_remember, BACKUP_CODE_LENGTH) - binding.backupCodeLayout.visibility = View.VISIBLE - binding.confirmCodeLabel.visibility = View.VISIBLE - binding.confirmCodeLayout.visibility = View.VISIBLE - binding.welcomeBackLayout.visibility = View.GONE + setUpInitialState( + titleResId = R.string.connect_backup_code_title_set, + showConfirmCode = true, + subtitle = getString(R.string.connect_backup_code_remember, BACKUP_CODE_LENGTH), + ) } } @@ -88,7 +75,7 @@ class PersonalIdBackupCodeFragment : BasePersonalIdBackupCodeFragment() { private fun handleNotMeButtonPressed() { personalIdSessionData.accountExists = false clearBackupCodeFields() - configureUiByMode() + setUpView() } override fun handleBackupCodeSubmission() { diff --git a/app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt b/app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt index c44b176f56..5702d0fe4e 100644 --- a/app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt +++ b/app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt @@ -26,28 +26,13 @@ class PersonalIdProfileBackupCodeFragment : BasePersonalIdBackupCodeFragment() { } } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View { - val view = super.onCreateView(inflater, container, savedInstanceState) - setUpView() - return view - } - - fun setUpView() { - requireActivity().title = getString(R.string.connect_backup_code_title_confirm) - binding.recoveryCodeTilte.setText(R.string.connect_backup_code_title_confirm) - binding.backupCodeSubtitle.setText(R.string.connect_backup_code_message) - binding.backupCodeLayout.visibility = View.VISIBLE - binding.confirmCodeLayout.visibility = View.GONE - binding.confirmCodeLabel.visibility = View.GONE - binding.welcomeBackLayout.visibility = View.GONE - forgotBackupCodeButton.visibility = View.VISIBLE - forgotBackupCodeButton.setText(R.string.personalid_forgot_backup_code) - enableContinueButton(false) - setupListeners() + override fun setUpView() { + setUpInitialState( + titleResId = R.string.connect_backup_code_title_confirm, + showConfirmCode = false, + subtitle = getString(R.string.connect_backup_code_message), + notMeButtonTextId = R.string.personalid_forgot_backup_code + ) } override fun onCodeChanged() { From 801e8f55a283e0482318b7e8a525a5218db55c67 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 31 Aug 2026 12:16:18 +0530 Subject: [PATCH 08/10] Fix return value for backup code validation --- .../fragments/personalId/BasePersonalIdBackupCodeFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt b/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt index 8eed4e2534..754e109891 100644 --- a/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt +++ b/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt @@ -118,7 +118,7 @@ abstract class BasePersonalIdBackupCodeFragment : BasePersonalIdFragment() { val isBackupCodeComplete = backupCode.length == BACKUP_CODE_LENGTH if (binding.confirmCodeLayout.visibility != View.VISIBLE) { enableContinueButton(isBackupCodeComplete) - return true + return isBackupCodeComplete } val confirmCode = binding.confirmCodeView.codeValue val isConfirmCodeComplete = confirmCode.length == BACKUP_CODE_LENGTH From 5a7b3b7b24f8ecf7f72baae8d97d552f9b6d568b Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 31 Aug 2026 12:53:46 +0530 Subject: [PATCH 09/10] ktlint --- .../fragments/personalId/BasePersonalIdBackupCodeFragment.kt | 2 +- .../personalId/profile/PersonalIdProfileBackupCodeFragment.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt b/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt index 754e109891..8fb0319f84 100644 --- a/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt +++ b/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt @@ -73,7 +73,7 @@ abstract class BasePersonalIdBackupCodeFragment : BasePersonalIdFragment() { titleResId: Int, showConfirmCode: Boolean, subtitle: CharSequence, - notMeButtonTextId : Int? = null, + notMeButtonTextId: Int? = null, ) { requireActivity().title = getString(titleResId) binding.recoveryCodeTilte.setText(titleResId) diff --git a/app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt b/app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt index 5702d0fe4e..767adb788d 100644 --- a/app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt +++ b/app/src/org/commcare/personalId/profile/PersonalIdProfileBackupCodeFragment.kt @@ -31,7 +31,7 @@ class PersonalIdProfileBackupCodeFragment : BasePersonalIdBackupCodeFragment() { titleResId = R.string.connect_backup_code_title_confirm, showConfirmCode = false, subtitle = getString(R.string.connect_backup_code_message), - notMeButtonTextId = R.string.personalid_forgot_backup_code + notMeButtonTextId = R.string.personalid_forgot_backup_code, ) } From fc9c9116ad06b439b87df2d75e3d037ea06da24f Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 31 Aug 2026 15:01:01 +0530 Subject: [PATCH 10/10] remvove view code from validator --- .../fragments/personalId/BasePersonalIdBackupCodeFragment.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt b/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt index 8fb0319f84..326476bb14 100644 --- a/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt +++ b/app/src/org/commcare/fragments/personalId/BasePersonalIdBackupCodeFragment.kt @@ -117,7 +117,6 @@ abstract class BasePersonalIdBackupCodeFragment : BasePersonalIdFragment() { val backupCode = binding.backupCodeView.codeValue val isBackupCodeComplete = backupCode.length == BACKUP_CODE_LENGTH if (binding.confirmCodeLayout.visibility != View.VISIBLE) { - enableContinueButton(isBackupCodeComplete) return isBackupCodeComplete } val confirmCode = binding.confirmCodeView.codeValue @@ -128,7 +127,6 @@ abstract class BasePersonalIdBackupCodeFragment : BasePersonalIdFragment() { clearError() } val isValid = isBackupCodeComplete && backupCode == confirmCode - enableContinueButton(isValid) return isValid }