From 460137160e83a6c72b4f612d27743b6352fb2694 Mon Sep 17 00:00:00 2001
From: alexandr-simprints
<129499142+alexandr-simprints@users.noreply.github.com>
Date: Wed, 11 Feb 2026 10:34:10 +0000
Subject: [PATCH 1/7] [MS-1341] Adding on tap focus for the OCR scanner (#1581)
* [MS-1341] Adding on tap focus for the OCR scanner
* [MS-1341] Removing 'camera' field and verifying lifecycle state before using binding
(cherry picked from commit 9c1e0174f6a2220e4b8cdd7088e54e9bb26f7fad)
---
.../scanocr/ExternalCredentialScanOcrFragment.kt | 12 ++++++++++++
.../scanocr/usecase/ProvideCameraListenerUseCase.kt | 5 ++++-
2 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt
index e09a0240f6..62285e6ed5 100644
--- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt
+++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt
@@ -19,6 +19,7 @@ import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
+import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@@ -38,6 +39,7 @@ import com.simprints.feature.externalcredential.screens.scanocr.usecase.ProvideC
import com.simprints.feature.externalcredential.screens.search.model.ScannedCredential
import com.simprints.infra.logging.LoggingConstants.CrashReportTag.MULTI_FACTOR_ID
import com.simprints.infra.logging.Simber
+import com.simprints.infra.uibase.camera.qrscan.CameraFocusManager
import com.simprints.infra.uibase.navigation.navigateSafely
import com.simprints.infra.uibase.view.applySystemBarInsets
import com.simprints.infra.uibase.view.fadeIn
@@ -97,6 +99,9 @@ internal class ExternalCredentialScanOcrFragment : Fragment(R.layout.fragment_ex
@Inject
lateinit var provideCameraListenerUseCase: ProvideCameraListenerUseCase
+ @Inject
+ lateinit var cameraFocusManagerFactory: CameraFocusManager.Factory
+
@Inject
@DispatcherBG
lateinit var bgDispatcher: CoroutineDispatcher
@@ -197,6 +202,13 @@ internal class ExternalCredentialScanOcrFragment : Fragment(R.layout.fragment_ex
onImageCaptureReady = { capture ->
imageCapture = capture
},
+ onCameraReady = { camera ->
+ if (lifecycle.currentState == Lifecycle.State.RESUMED) {
+ val cameraFocusManager = cameraFocusManagerFactory.create(MULTI_FACTOR_ID)
+ cameraFocusManager.setUpFocusOnTap(binding.preview, camera)
+ cameraFocusManager.setUpAutoFocus(binding.preview, camera)
+ }
+ },
)
cameraProviderFuture.addListener(cameraListener, ContextCompat.getMainExecutor(requireContext()))
}
diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/ProvideCameraListenerUseCase.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/ProvideCameraListenerUseCase.kt
index 8e0f1c09aa..fb9d8ceb22 100644
--- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/ProvideCameraListenerUseCase.kt
+++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/ProvideCameraListenerUseCase.kt
@@ -1,6 +1,7 @@
package com.simprints.feature.externalcredential.screens.scanocr.usecase
import androidx.camera.core.AspectRatio
+import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
@@ -21,6 +22,7 @@ internal class ProvideCameraListenerUseCase @Inject constructor() {
viewLifecycleOwner: LifecycleOwner,
onImageAnalysisReady: (ImageAnalysis) -> Unit,
onImageCaptureReady: (ImageCapture) -> Unit,
+ onCameraReady: (Camera) -> Unit,
) = Runnable {
val cameraProvider = cameraProviderFuture.get()
val aspectRatio = AspectRatio.RATIO_16_9
@@ -48,9 +50,10 @@ internal class ProvideCameraListenerUseCase @Inject constructor() {
try {
cameraProvider.unbindAll()
- cameraProvider.bindToLifecycle(viewLifecycleOwner, cameraSelector, preview, imageCapture, imageAnalysis)
+ val camera = cameraProvider.bindToLifecycle(viewLifecycleOwner, cameraSelector, preview, imageCapture, imageAnalysis)
onImageAnalysisReady(imageAnalysis)
onImageCaptureReady(imageCapture)
+ onCameraReady(camera)
} catch (e: Exception) {
Simber.e("Camera binding failed in OCR", e, MULTI_FACTOR_ID)
}
From 086ab1b6d1c3caa3417d04311267459ec99458c9 Mon Sep 17 00:00:00 2001
From: alexandr-simprints
<129499142+alexandr-simprints@users.noreply.github.com>
Date: Thu, 12 Feb 2026 14:58:08 +0200
Subject: [PATCH 2/7] [MS-1338] MFID: removing the post-match confirmation
checkbox (#1582)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Cherry picking latest linter rulesˆ from the main branch
* [MS-1338] Removing checkbox from the credential confirmation
(cherry picked from commit 376ca5db6ad0056b6b7bfb2b8205179d1792efe4)
---
.../view/ScannedCredentialDialog.kt | 5 -----
.../res/layout/dialog_scanned_credential.xml | 18 ++++++------------
2 files changed, 6 insertions(+), 17 deletions(-)
diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/view/ScannedCredentialDialog.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/view/ScannedCredentialDialog.kt
index 9fd5837eff..ad996aef1a 100644
--- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/view/ScannedCredentialDialog.kt
+++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/view/ScannedCredentialDialog.kt
@@ -52,11 +52,6 @@ class ScannedCredentialDialog(
documentField.text = credentialField
title.text = context.getString(IDR.string.mfid_add_document_title, credential)
credentialValue.text = displayedCredential.value
- confirmCredentialCheckbox.text =
- context.getString(IDR.string.mfid_confirmation_checkbox_text, credentialField)
- confirmCredentialCheckbox.setOnCheckedChangeListener { _, isChecked ->
- buttonConfirm.isEnabled = isChecked
- }
buttonSkip.setOnClickListener { onSkip() }
buttonConfirm.setOnClickListener { onConfirm() }
}
diff --git a/feature/external-credential/src/main/res/layout/dialog_scanned_credential.xml b/feature/external-credential/src/main/res/layout/dialog_scanned_credential.xml
index 5a1ec274d8..4ee42c7014 100644
--- a/feature/external-credential/src/main/res/layout/dialog_scanned_credential.xml
+++ b/feature/external-credential/src/main/res/layout/dialog_scanned_credential.xml
@@ -49,22 +49,17 @@
style="@style/Text.Headline4.Bold"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/margin_huge"
android:layout_marginEnd="@dimen/margin_large"
android:paddingHorizontal="@dimen/margin_large"
android:textColor="@color/simprints_text_black"
tools:text="GHA-123456789-0" />
-
+
Date: Tue, 17 Feb 2026 12:49:16 +0200
Subject: [PATCH 3/7] [MS-1339] MFID: add `HAS_NUMBER_NO_ID` skip reason
(#1592)
* [MS-1339] Adding 'Has number, no card (booklet)' skip reason for MFID
* [MS-1339] Adding `HAS_NUMBER_NO_ID` credential skip reason
(cherry picked from commit cefefb6e0c73b8c53c96d9e9c73012149c4637d6)
---
.../screens/skip/ExternalCredentialSkipFragment.kt | 6 +++++-
.../main/res/layout/fragment_external_credential_skip.xml | 7 +++++++
.../remote/models/ApiExternalCredentialSelectionPayload.kt | 2 ++
.../domain/models/ExternalCredentialSelectionEvent.kt | 1 +
infra/resources/src/main/res/values/strings.xml | 1 +
5 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/skip/ExternalCredentialSkipFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/skip/ExternalCredentialSkipFragment.kt
index e0e7675cc3..fe36111d32 100644
--- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/skip/ExternalCredentialSkipFragment.kt
+++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/skip/ExternalCredentialSkipFragment.kt
@@ -42,6 +42,7 @@ class ExternalCredentialSkipFragment : Fragment(R.layout.fragment_external_crede
private fun initViews(credentialTypes: List) = with(binding) {
mapOf(
title to IDR.string.mfid_skip_title,
+ skipReasonHasNumberNoId to IDR.string.mfid_skip_reason_has_number_no_id,
skipReasonDoesNotHaveDocument to IDR.string.mfid_skip_reason_does_not_have,
skipReasonDidNotBring to IDR.string.mfid_skip_reason_did_not_bring,
skipReasonIncorrect to IDR.string.mfid_skip_reason_incorrect,
@@ -67,7 +68,9 @@ class ExternalCredentialSkipFragment : Fragment(R.layout.fragment_external_crede
reasonTextInput.text.toString().isNotEmpty()
}
- else -> true
+ else -> {
+ true
+ }
}
buttonSkip.isEnabled = isSkipButtonEnabled
}
@@ -95,6 +98,7 @@ class ExternalCredentialSkipFragment : Fragment(R.layout.fragment_external_crede
}
private fun viewIdToOption(checkedId: Int) = when (checkedId) {
+ R.id.skipReasonHasNumberNoId -> ExternalCredentialSelectionEvent.SkipReason.HAS_NUMBER_NO_ID
R.id.skipReasonDoesNotHaveDocument -> ExternalCredentialSelectionEvent.SkipReason.DOES_NOT_HAVE_ID
R.id.skipReasonDidNotBring -> ExternalCredentialSelectionEvent.SkipReason.DID_NOT_BRING_ID
R.id.skipReasonIncorrect -> ExternalCredentialSelectionEvent.SkipReason.BROUGHT_INCORRECT_ID
diff --git a/feature/external-credential/src/main/res/layout/fragment_external_credential_skip.xml b/feature/external-credential/src/main/res/layout/fragment_external_credential_skip.xml
index 25d8233497..38ff3e0a18 100644
--- a/feature/external-credential/src/main/res/layout/fragment_external_credential_skip.xml
+++ b/feature/external-credential/src/main/res/layout/fragment_external_credential_skip.xml
@@ -53,6 +53,13 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
+
+
ApiExternalCredentialSkipReason.HAS_NUMBER_NO_ID
SkipReason.DOES_NOT_HAVE_ID -> ApiExternalCredentialSkipReason.DOES_NOT_HAVE_ID
SkipReason.DID_NOT_BRING_ID -> ApiExternalCredentialSkipReason.DID_NOT_BRING_ID
SkipReason.BROUGHT_INCORRECT_ID -> ApiExternalCredentialSkipReason.BROUGHT_INCORRECT_ID
diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/ExternalCredentialSelectionEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/ExternalCredentialSelectionEvent.kt
index 8781c75588..6da1c5ef8b 100644
--- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/ExternalCredentialSelectionEvent.kt
+++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/ExternalCredentialSelectionEvent.kt
@@ -84,6 +84,7 @@ data class ExternalCredentialSelectionEvent(
@Keep
@Serializable
enum class SkipReason {
+ HAS_NUMBER_NO_ID,
DOES_NOT_HAVE_ID,
DID_NOT_BRING_ID,
BROUGHT_INCORRECT_ID,
diff --git a/infra/resources/src/main/res/values/strings.xml b/infra/resources/src/main/res/values/strings.xml
index bdbf1ccd5d..2e949b63ec 100644
--- a/infra/resources/src/main/res/values/strings.xml
+++ b/infra/resources/src/main/res/values/strings.xml
@@ -530,6 +530,7 @@
Add this %1$s?
Why did you skip the %1$s scan?
+ Has number, no %1$s (Booklet)
Does not have %1$s
Did not bring %1$s
Brought incorrect %1$s
From d00f03b49bdf7fee469b75c127898d5b91d50038 Mon Sep 17 00:00:00 2001
From: alexandr-simprints
<129499142+alexandr-simprints@users.noreply.github.com>
Date: Tue, 17 Feb 2026 12:49:43 +0200
Subject: [PATCH 4/7] [MS-1351] Adding system bar insets for
`ExternalCredentialControllerFragment` (#1593)
* [MS-1351] Adding system bar insets for ExternalCredentialControllerFragment
* [MS-1351] Adding system bar insets for fragments in the MFID workflow
(cherry picked from commit 655d362809a13a66bfd951df241546a5eec7326c)
---
.../screens/controller/ExternalCredentialControllerFragment.kt | 1 -
.../screens/search/ExternalCredentialSearchFragment.kt | 2 ++
.../screens/skip/ExternalCredentialSkipFragment.kt | 3 ++-
3 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt
index 79a667b186..e674d154f9 100644
--- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt
+++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt
@@ -39,7 +39,6 @@ internal class ExternalCredentialControllerFragment : Fragment(R.layout.fragment
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)
-
viewModel.init(params)
findNavController().handleResult(
diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchFragment.kt
index 9df99a4d9b..330cfff174 100644
--- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchFragment.kt
+++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchFragment.kt
@@ -31,6 +31,7 @@ import com.simprints.feature.externalcredential.screens.search.model.SearchState
import com.simprints.infra.logging.LoggingConstants.CrashReportTag.MULTI_FACTOR_ID
import com.simprints.infra.logging.Simber
import com.simprints.infra.uibase.navigation.navigateSafely
+import com.simprints.infra.uibase.view.applySystemBarInsets
import com.simprints.infra.uibase.viewbinding.viewBinding
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -65,6 +66,7 @@ internal class ExternalCredentialSearchFragment : Fragment(R.layout.fragment_ext
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)
+ applySystemBarInsets(view)
initObservers()
}
diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/skip/ExternalCredentialSkipFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/skip/ExternalCredentialSkipFragment.kt
index fe36111d32..d066dec19e 100644
--- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/skip/ExternalCredentialSkipFragment.kt
+++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/skip/ExternalCredentialSkipFragment.kt
@@ -14,6 +14,7 @@ import com.simprints.feature.externalcredential.databinding.FragmentExternalCred
import com.simprints.feature.externalcredential.ext.getCredentialTypeString
import com.simprints.feature.externalcredential.screens.controller.ExternalCredentialViewModel
import com.simprints.infra.events.event.domain.models.ExternalCredentialSelectionEvent
+import com.simprints.infra.uibase.view.applySystemBarInsets
import com.simprints.infra.uibase.viewbinding.viewBinding
import dagger.hilt.android.AndroidEntryPoint
import com.simprints.infra.resources.R as IDR
@@ -28,7 +29,7 @@ class ExternalCredentialSkipFragment : Fragment(R.layout.fragment_external_crede
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)
-
+ applySystemBarInsets(view)
initObservers()
}
From 71789d0786a529952ed2ff36c3c07c701ffe072f Mon Sep 17 00:00:00 2001
From: alexandr-simprints
<129499142+alexandr-simprints@users.noreply.github.com>
Date: Tue, 17 Feb 2026 12:50:00 +0200
Subject: [PATCH 5/7] [MS-1340] MFID: adding validation for manual credential
entry (#1584)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Cherry picking latest linter rulesˆ from the main branch
* [MS-1340] Adding validation for the credential fields edit text
* [MS-1340] Updating tests
* [MS-1340] Removing existing text watch when rendering updated state
* [MS-1340] Fixing code comments
* [MS-1340] Confirmation checkbox is disabled if credential is being edited
* [MS-1340] Renaming credential validation use cases for better readability
(cherry picked from commit 295763e141852f2505be903798e771917db12b89)
---
.../usecase/GhanaIdCardOcrSelectorUseCase.kt | 2 +-
.../ExternalCredentialSearchFragment.kt | 40 ++++++++++---
.../ExternalCredentialSearchViewModel.kt | 26 ++++++++
.../search/model/SearchCredentialState.kt | 3 +
.../src/main/res/color/checkbox_color.xml | 5 ++
.../src/main/res/color/ic_edit_color.xml | 5 ++
.../src/main/res/drawable/ic_done.xml | 2 +-
.../fragment_external_credential_search.xml | 2 +-
.../ExternalCredentialSearchViewModelTest.kt | 60 +++++++++++++++++++
9 files changed, 133 insertions(+), 12 deletions(-)
create mode 100644 feature/external-credential/src/main/res/color/checkbox_color.xml
create mode 100644 feature/external-credential/src/main/res/color/ic_edit_color.xml
diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/GhanaIdCardOcrSelectorUseCase.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/GhanaIdCardOcrSelectorUseCase.kt
index b5672f17fa..5c6b74f3d2 100644
--- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/GhanaIdCardOcrSelectorUseCase.kt
+++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/GhanaIdCardOcrSelectorUseCase.kt
@@ -6,7 +6,7 @@ internal class GhanaIdCardOcrSelectorUseCase @Inject constructor() {
operator fun invoke(readoutValue: String): Boolean = GHANA_ID_PATTERN.matches(readoutValue)
companion object {
- // Ghana ID card number pattern is "GHA-12345789-0"
+ // Ghana ID card number pattern is "GHA-123456789-0"
private val GHANA_ID_PATTERN = Regex("^GHA-\\d{9}-\\d$")
}
}
diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchFragment.kt
index 330cfff174..2bc007c62c 100644
--- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchFragment.kt
+++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchFragment.kt
@@ -3,10 +3,12 @@ package com.simprints.feature.externalcredential.screens.search
import android.content.Context
import android.graphics.BitmapFactory
import android.os.Bundle
+import android.text.TextWatcher
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
+import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
@@ -59,7 +61,7 @@ internal class ExternalCredentialSearchFragment : Fragment(R.layout.fragment_ext
@Inject
lateinit var zoomOntoCredentialUseCase: ZoomOntoCredentialUseCase
- private var isEditingCredential: Boolean = false
+ private var credentialTextWatcher: TextWatcher? = null
override fun onViewCreated(
view: View,
@@ -94,7 +96,9 @@ internal class ExternalCredentialSearchFragment : Fragment(R.layout.fragment_ext
val credentialType = state.scannedCredential.credentialType
val credentialField = resources.getCredentialFieldTitle(credentialType)
val currentEditTextValue = credentialEditText.text.toString()
+ val isEditingCredential = state.isEditingCredential
renderImage(state.scannedCredential)
+ renderCredentialEdit(state)
credential.takeIf { currentEditTextValue.isEmpty() }?.let {
credentialEditText.setText(it) // Setting only once at the start
}
@@ -104,18 +108,25 @@ internal class ExternalCredentialSearchFragment : Fragment(R.layout.fragment_ext
credentialValue.text = currentEditTextValue
confirmCredentialCheckbox.isVisible = state.searchState != SearchState.Searching
confirmCredentialCheckbox.text = getString(IDR.string.mfid_confirmation_checkbox_text, credentialField)
- confirmCredentialCheckbox.isChecked = state.isConfirmed
+ confirmCredentialCheckbox.isChecked = state.isConfirmed && !state.isEditingCredential
+ confirmCredentialCheckbox.isEnabled = !state.isEditingCredential
iconEditCredential.setOnClickListener {
- viewModel.updateConfirmation(isConfirmed = false)
- toggleCredentialEdit()
- if (!isEditingCredential) {
- viewModel.confirmCredentialUpdate(credentialEditText.text.toString().asTokenizableRaw())
+ if (isEditingCredential) {
+ viewModel.confirmCredentialUpdate(updatedCredential = credentialEditText.text.toString().asTokenizableRaw())
}
+ viewModel.updateIsEditingCredential(isEditing = !isEditingCredential)
}
confirmCredentialCheckbox.setOnCheckedChangeListener { _, checkedId ->
viewModel.updateConfirmation(isConfirmed = checkedId)
}
+
+ credentialTextWatcher?.let(credentialEditText::removeTextChangedListener)
+ credentialTextWatcher = credentialEditText.addTextChangedListener(
+ afterTextChanged = { _ ->
+ renderEditIcon(isEditingCredential)
+ },
+ )
}
private fun renderSearchProgress(
@@ -198,7 +209,7 @@ internal class ExternalCredentialSearchFragment : Fragment(R.layout.fragment_ext
val isSearching = state.searchState != SearchState.Searching
buttonRecapture.isVisible = isSearching
buttonConfirm.isVisible = isSearching
- buttonConfirm.isEnabled = state.isConfirmed
+ buttonConfirm.isEnabled = state.isConfirmed && !state.isEditingCredential
viewModel.getButtonTextResource(state.searchState, state.flowType)?.run(buttonConfirm::setText)
buttonConfirm.setOnClickListener {
viewModel.finish(state)
@@ -226,8 +237,9 @@ internal class ExternalCredentialSearchFragment : Fragment(R.layout.fragment_ext
}
}
- private fun toggleCredentialEdit() = with(binding) {
- isEditingCredential = !isEditingCredential
+ private fun renderCredentialEdit(state: SearchCredentialState) = with(binding) {
+ val isEditingCredential = state.isEditingCredential
+ renderEditIcon(isEditingCredential)
val iconRes = if (isEditingCredential) {
R.drawable.ic_done
} else {
@@ -250,4 +262,14 @@ internal class ExternalCredentialSearchFragment : Fragment(R.layout.fragment_ext
private fun hideKeyboard() {
requireActivity().hideKeyboard()
}
+
+ private fun renderEditIcon(isEditingCredential: Boolean) = with(binding) {
+ val isEditIconEnabled = if (isEditingCredential) {
+ viewModel.isCredentialFormatValid(credentialEditText.text?.toString())
+ } else {
+ true
+ }
+ iconEditCredential.alpha = if (isEditIconEnabled) 1.0f else 0.5f
+ iconEditCredential.isEnabled = isEditIconEnabled
+ }
}
diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModel.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModel.kt
index cbc921aaa9..e3bb78b393 100644
--- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModel.kt
+++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModel.kt
@@ -13,6 +13,8 @@ import com.simprints.core.livedata.send
import com.simprints.core.tools.time.TimeHelper
import com.simprints.feature.externalcredential.ExternalCredentialSearchResult
import com.simprints.feature.externalcredential.model.ExternalCredentialParams
+import com.simprints.feature.externalcredential.screens.scanocr.usecase.GhanaIdCardOcrSelectorUseCase
+import com.simprints.feature.externalcredential.screens.scanocr.usecase.GhanaNhisCardOcrSelectorUseCase
import com.simprints.feature.externalcredential.screens.search.model.ScannedCredential
import com.simprints.feature.externalcredential.screens.search.model.SearchCredentialState
import com.simprints.feature.externalcredential.screens.search.model.SearchState
@@ -40,6 +42,8 @@ internal class ExternalCredentialSearchViewModel @AssistedInject constructor(
private val tokenizationProcessor: TokenizationProcessor,
private val enrolmentRecordRepository: EnrolmentRecordRepository,
private val eventsTracker: ExternalCredentialEventTrackerUseCase,
+ private val ghanaIdValidationUseCase: GhanaIdCardOcrSelectorUseCase,
+ private val ghanaNhisCardValidationUseCase: GhanaNhisCardOcrSelectorUseCase,
) : ViewModel() {
@AssistedFactory
interface Factory {
@@ -80,6 +84,10 @@ internal class ExternalCredentialSearchViewModel @AssistedInject constructor(
updateState { it.copy(isConfirmed = isConfirmed) }
}
+ fun updateIsEditingCredential(isEditing: Boolean) {
+ updateState { it.copy(isEditingCredential = isEditing) }
+ }
+
fun confirmCredentialUpdate(updatedCredential: TokenizableString.Raw) {
viewModelScope.launch {
configRepository.getProject()?.let { project ->
@@ -206,4 +214,22 @@ internal class ExternalCredentialSearchViewModel @AssistedInject constructor(
)
}
}
+
+ fun isCredentialFormatValid(credential: String?): Boolean {
+ if (credential == null) return false
+ return when (scannedCredential.credentialType) {
+ ExternalCredentialType.NHISCard -> {
+ // 8 digits
+ ghanaNhisCardValidationUseCase(credential)
+ }
+ ExternalCredentialType.GhanaIdCard -> {
+ // Ghana ID card number pattern is "GHA-123456789-0"
+ ghanaIdValidationUseCase(credential)
+ }
+ ExternalCredentialType.QRCode -> {
+ // No QR code validation as of 2025.4.1
+ true
+ }
+ }
+ }
}
diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/SearchCredentialState.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/SearchCredentialState.kt
index 4493365467..fee2e741da 100644
--- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/SearchCredentialState.kt
+++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/SearchCredentialState.kt
@@ -5,6 +5,7 @@ import com.simprints.core.ExcludedFromGeneratedTestCoverageReports
import com.simprints.core.domain.common.FlowType
import com.simprints.core.domain.tokenization.TokenizableString
import com.simprints.feature.externalcredential.model.CredentialMatch
+import kotlin.Boolean
@Keep
@ExcludedFromGeneratedTestCoverageReports("Data struct")
@@ -14,6 +15,7 @@ internal data class SearchCredentialState(
val flowType: FlowType,
val searchState: SearchState,
val isConfirmed: Boolean,
+ val isEditingCredential: Boolean,
) {
companion object {
fun buildInitial(
@@ -25,6 +27,7 @@ internal data class SearchCredentialState(
flowType = flowType,
searchState = SearchState.Searching,
isConfirmed = false,
+ isEditingCredential = false,
)
}
}
diff --git a/feature/external-credential/src/main/res/color/checkbox_color.xml b/feature/external-credential/src/main/res/color/checkbox_color.xml
new file mode 100644
index 0000000000..2f81cf52fc
--- /dev/null
+++ b/feature/external-credential/src/main/res/color/checkbox_color.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/feature/external-credential/src/main/res/color/ic_edit_color.xml b/feature/external-credential/src/main/res/color/ic_edit_color.xml
new file mode 100644
index 0000000000..982d0cb05b
--- /dev/null
+++ b/feature/external-credential/src/main/res/color/ic_edit_color.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/feature/external-credential/src/main/res/drawable/ic_done.xml b/feature/external-credential/src/main/res/drawable/ic_done.xml
index c7ea9bc53a..6b7bc1618e 100644
--- a/feature/external-credential/src/main/res/drawable/ic_done.xml
+++ b/feature/external-credential/src/main/res/drawable/ic_done.xml
@@ -4,6 +4,6 @@
android:viewportWidth="960"
android:viewportHeight="960">
diff --git a/feature/external-credential/src/main/res/layout/fragment_external_credential_search.xml b/feature/external-credential/src/main/res/layout/fragment_external_credential_search.xml
index 96b349c4a5..3675099657 100644
--- a/feature/external-credential/src/main/res/layout/fragment_external_credential_search.xml
+++ b/feature/external-credential/src/main/res/layout/fragment_external_credential_search.xml
@@ -109,7 +109,7 @@
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_marginBottom="@dimen/margin_large"
- android:buttonTint="@color/simprints_blue"
+ android:buttonTint="@color/checkbox_color"
android:checked="false"
android:paddingStart="@dimen/margin_default"
android:visibility="gone"
diff --git a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModelTest.kt b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModelTest.kt
index cf905cfc3c..49ebabf282 100644
--- a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModelTest.kt
+++ b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModelTest.kt
@@ -12,6 +12,8 @@ import com.simprints.core.tools.time.TimeHelper
import com.simprints.core.tools.time.Timestamp
import com.simprints.feature.externalcredential.model.CredentialMatch
import com.simprints.feature.externalcredential.model.ExternalCredentialParams
+import com.simprints.feature.externalcredential.screens.scanocr.usecase.GhanaIdCardOcrSelectorUseCase
+import com.simprints.feature.externalcredential.screens.scanocr.usecase.GhanaNhisCardOcrSelectorUseCase
import com.simprints.feature.externalcredential.screens.search.model.ScannedCredential
import com.simprints.feature.externalcredential.screens.search.model.SearchCredentialState
import com.simprints.feature.externalcredential.screens.search.model.SearchState
@@ -75,6 +77,12 @@ internal class ExternalCredentialSearchViewModelTest {
@MockK
lateinit var eventsTracker: ExternalCredentialEventTrackerUseCase
+ @MockK
+ lateinit var ghanaIdValidationUseCase: GhanaIdCardOcrSelectorUseCase
+
+ @MockK
+ lateinit var ghanaNhisCardValidationUseCase: GhanaNhisCardOcrSelectorUseCase
+
private lateinit var viewModel: ExternalCredentialSearchViewModel
@Before
@@ -96,6 +104,8 @@ internal class ExternalCredentialSearchViewModelTest {
tokenizationProcessor = tokenizationProcessor,
enrolmentRecordRepository = enrolmentRecordRepository,
eventsTracker = eventsTracker,
+ ghanaIdValidationUseCase = ghanaIdValidationUseCase,
+ ghanaNhisCardValidationUseCase = ghanaNhisCardValidationUseCase,
)
@Test
@@ -314,4 +324,54 @@ internal class ExternalCredentialSearchViewModelTest {
assertThat(viewModel.stateLiveData.value?.displayedCredential).isEqualTo(decryptedCredential)
coVerify { tokenizationProcessor.decrypt(encryptedCredential, TokenKeyType.ExternalCredential, project) }
}
+
+ @Test
+ fun `isCredentialFormatValid validates NHIS card format`() = runTest {
+ val validNhisCard = "12345678"
+ val invalidNhisCard = "invalid"
+
+ every { mockScannedCredential.credentialType } returns ExternalCredentialType.NHISCard
+ every { ghanaNhisCardValidationUseCase(validNhisCard) } returns true
+ every { ghanaNhisCardValidationUseCase(invalidNhisCard) } returns false
+
+ viewModel = createViewModel()
+
+ assertThat(viewModel.isCredentialFormatValid(validNhisCard)).isTrue()
+ assertThat(viewModel.isCredentialFormatValid(invalidNhisCard)).isFalse()
+ assertThat(viewModel.isCredentialFormatValid(null)).isFalse()
+ verify { ghanaNhisCardValidationUseCase(validNhisCard) }
+ verify { ghanaNhisCardValidationUseCase(invalidNhisCard) }
+ }
+
+ @Test
+ fun `isCredentialFormatValid validates Ghana ID card format`() = runTest {
+ val validGhanaIdCard = "GHA-12345789-0"
+ val invalidGhanaIdCard = "invalid"
+
+ every { mockScannedCredential.credentialType } returns ExternalCredentialType.GhanaIdCard
+ every { ghanaIdValidationUseCase(validGhanaIdCard) } returns true
+ every { ghanaIdValidationUseCase(invalidGhanaIdCard) } returns false
+
+ viewModel = createViewModel()
+
+ assertThat(viewModel.isCredentialFormatValid(validGhanaIdCard)).isTrue()
+ assertThat(viewModel.isCredentialFormatValid(invalidGhanaIdCard)).isFalse()
+ assertThat(viewModel.isCredentialFormatValid(null)).isFalse()
+ verify { ghanaIdValidationUseCase(validGhanaIdCard) }
+ verify { ghanaIdValidationUseCase(invalidGhanaIdCard) }
+ }
+
+ @Test
+ fun `isCredentialFormatValid always returns true for QR code`() = runTest {
+ val anyValue = "any_value"
+ val emptyValue = ""
+
+ every { mockScannedCredential.credentialType } returns ExternalCredentialType.QRCode
+
+ viewModel = createViewModel()
+
+ assertThat(viewModel.isCredentialFormatValid(anyValue)).isTrue()
+ assertThat(viewModel.isCredentialFormatValid(emptyValue)).isTrue()
+ assertThat(viewModel.isCredentialFormatValid(null)).isFalse()
+ }
}
From f2863608b5bb636f70cc4e3ec235d46fa59fe498 Mon Sep 17 00:00:00 2001
From: alexandr-simprints
<129499142+alexandr-simprints@users.noreply.github.com>
Date: Tue, 17 Feb 2026 10:50:15 +0000
Subject: [PATCH 6/7] [MS-1346] Identification response now contains scanned
external credential data (#1591)
* [MS-1346] Identificaiton response now contains scanned external credential data
* [MS-1346] Renaming variables to be more consistent
* [MS-1346] Fixing tests
* [MS-1346] Fixing tests, removing unnecessary variables
* [MS-1346] Fixing tests
* [MS-1346] Fixing tests
* [MS-1346] Adding test coverage for credential response in Identification response
(cherry picked from commit b12990ed4c14a1d9da3706b848491717096a2b64)
---
.../feature/clientapi/ClientApiViewModel.kt | 1 +
.../response/LibSimprintsResponseMapper.kt | 2 +-
.../clientapi/ClientApiViewModelTest.kt | 1 +
.../response/CommCareResponseMapperTest.kt | 1 +
.../LibSimprintsResponseMapperTest.kt | 77 +++++++++++++++++++
.../mappers/response/OdkResponseMapperTest.kt | 2 +
.../MapRefusalOrErrorResultUseCase.kt | 9 ++-
.../response/CreateIdentifyResponseUseCase.kt | 17 ++++
.../usecases/AddCallbackEventUseCaseTest.kt | 1 +
.../UpdateDailyActivityUseCaseTest.kt | 2 +-
.../CreateIdentifyResponseUseCaseTest.kt | 4 +
.../orchestration/data/ActionResponse.kt | 1 +
.../data/responses/AppIdentifyResponse.kt | 1 +
13 files changed, 116 insertions(+), 3 deletions(-)
diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/ClientApiViewModel.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/ClientApiViewModel.kt
index ffd42e3912..d4bca49638 100644
--- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/ClientApiViewModel.kt
+++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/ClientApiViewModel.kt
@@ -147,6 +147,7 @@ class ClientApiViewModel @Inject internal constructor(
sessionId = currentSessionId,
identifications = identifyResponse.identifications,
isMultiFactorIdEnabled = identifyResponse.isMultiFactorIdEnabled,
+ scannedCredential = identifyResponse.scannedCredential,
),
),
)
diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt
index 304ed19497..8a562534b0 100644
--- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt
+++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt
@@ -73,7 +73,7 @@ internal class LibSimprintsResponseMapper @Inject constructor(
.toJson(),
)
}
- }
+ }.appendExternalCredential(response.scannedCredential.takeIf { response.isMultiFactorIdEnabled })
is ActionResponse.ConfirmActionResponse -> {
bundleOf(
diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/ClientApiViewModelTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/ClientApiViewModelTest.kt
index 45596dcfb6..ecee946617 100644
--- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/ClientApiViewModelTest.kt
+++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/ClientApiViewModelTest.kt
@@ -190,6 +190,7 @@ internal class ClientApiViewModelTest {
mockk {
every { identifications } returns emptyList()
every { isMultiFactorIdEnabled } returns false
+ every { scannedCredential } returns null
},
)
diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/CommCareResponseMapperTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/CommCareResponseMapperTest.kt
index f9abf3fb49..53f615d29c 100644
--- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/CommCareResponseMapperTest.kt
+++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/CommCareResponseMapperTest.kt
@@ -61,6 +61,7 @@ class CommCareResponseMapperTest {
),
),
isMultiFactorIdEnabled = false,
+ scannedCredential = null,
),
)
diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapperTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapperTest.kt
index 5d3140035f..4aa18e2708 100644
--- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapperTest.kt
+++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapperTest.kt
@@ -1,5 +1,6 @@
package com.simprints.feature.clientapi.mappers.response
+import android.os.Bundle
import androidx.test.ext.junit.runners.*
import com.google.common.truth.Truth.*
import com.simprints.core.domain.externalcredential.ExternalCredentialType
@@ -16,6 +17,7 @@ import com.simprints.feature.clientapi.mappers.response.LibSimprintsResponseMapp
import com.simprints.feature.clientapi.mappers.response.LibSimprintsResponseMapper.Companion.SCANNED_CREDENTIAL_TYPE
import com.simprints.feature.clientapi.mappers.response.LibSimprintsResponseMapper.Companion.SCANNED_CREDENTIAL_VALUE
import com.simprints.infra.orchestration.data.ActionResponse
+import com.simprints.infra.orchestration.data.responses.AppExternalCredential
import com.simprints.infra.orchestration.data.responses.AppMatchResult
import com.simprints.libsimprints.Constants
import com.simprints.libsimprints.contracts.VersionsList
@@ -93,6 +95,7 @@ class LibSimprintsResponseMapperTest {
),
),
isMultiFactorIdEnabled = false,
+ scannedCredential = null,
),
)
@@ -121,6 +124,7 @@ class LibSimprintsResponseMapperTest {
),
),
isMultiFactorIdEnabled = false,
+ scannedCredential = null,
),
)
@@ -469,6 +473,7 @@ class LibSimprintsResponseMapperTest {
sessionId = "sessionId",
identifications = listOf(identification1, identification2),
isMultiFactorIdEnabled = true,
+ scannedCredential = null,
),
)
@@ -496,6 +501,7 @@ class LibSimprintsResponseMapperTest {
),
),
isMultiFactorIdEnabled = true,
+ scannedCredential = null,
),
)
@@ -523,6 +529,7 @@ class LibSimprintsResponseMapperTest {
),
),
isMultiFactorIdEnabled = false,
+ scannedCredential = null,
),
)
@@ -531,6 +538,76 @@ class LibSimprintsResponseMapperTest {
)
}
+ @Test
+ fun `when MFID is enabled, identify response contains scanned credential`() {
+ val expectedValue = "expectedValue".asTokenizableRaw()
+ val expectedType = ExternalCredentialType.NHISCard
+ val expectedJson = "{\"$SCANNED_CREDENTIAL_VALUE\":\"$expectedValue\",\"$SCANNED_CREDENTIAL_TYPE\":\"$expectedType\"}"
+ val scannedCredential = mockk {
+ every { value } returns expectedValue
+ every { type } returns expectedType
+ }
+
+ val extras = mapper(
+ createIdentifyActionResponse(
+ isMultiFactorIdEnabled = true,
+ scannedCredential = scannedCredential,
+ ),
+ )
+
+ assertCommonMfidIdentifyFields(extras)
+ assertThat(extras.getString(SCANNED_CREDENTIAL)).isEqualTo(expectedJson)
+ }
+
+ @Test
+ fun `when MFID is disabled, identify response does not contain credential`() {
+ val expectedValue = "expectedValue".asTokenizableRaw()
+ val expectedType = ExternalCredentialType.NHISCard
+ val scannedCredential = mockk {
+ every { value } returns expectedValue
+ every { type } returns expectedType
+ }
+
+ val extras = mapper(
+ createIdentifyActionResponse(
+ isMultiFactorIdEnabled = false,
+ scannedCredential = scannedCredential,
+ ),
+ )
+
+ assertCommonMfidIdentifyFields(extras)
+ assertThat(extras.keySet()).doesNotContain(SCANNED_CREDENTIAL)
+ }
+
+ // Helper functions
+ private fun createIdentifyActionResponse(
+ sessionId: String = "sessionId",
+ isMultiFactorIdEnabled: Boolean,
+ scannedCredential: AppExternalCredential? = null,
+ identifications: List = listOf(
+ AppMatchResult(
+ guid = "guid-1",
+ confidenceScore = 100,
+ matchConfidence = AppMatchConfidence.MEDIUM,
+ isLinkedToScannedCredential = true,
+ isCredentialVerified = true,
+ ),
+ ),
+ ) = ActionResponse.IdentifyActionResponse(
+ actionIdentifier = IdentifyRequestActionFactory.getIdentifier(),
+ sessionId = sessionId,
+ identifications = identifications,
+ isMultiFactorIdEnabled = isMultiFactorIdEnabled,
+ scannedCredential = scannedCredential,
+ )
+
+ private fun assertCommonMfidIdentifyFields(extras: Bundle) {
+ assertThat(extras.getString(Constants.SIMPRINTS_SESSION_ID)).isEqualTo("sessionId")
+ assertThat(extras.getString(Constants.SIMPRINTS_DEVICE_ID)).isEqualTo("deviceId")
+ assertThat(extras.getString(Constants.SIMPRINTS_APP_VERSION_NAME)).isEqualTo("appVersionName")
+ assertThat(extras.getBoolean(Constants.SIMPRINTS_BIOMETRICS_COMPLETE_CHECK)).isTrue()
+ }
+
private fun AppMatchResult.toResponseJson(): String {
val jsonBuilder = StringBuilder()
jsonBuilder.append("{\"guid\":\"$guid\"")
diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/OdkResponseMapperTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/OdkResponseMapperTest.kt
index 6b1349d889..2459de0d2e 100644
--- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/OdkResponseMapperTest.kt
+++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/OdkResponseMapperTest.kt
@@ -55,6 +55,7 @@ class OdkResponseMapperTest {
),
),
isMultiFactorIdEnabled = false,
+ scannedCredential = null,
),
)
@@ -75,6 +76,7 @@ class OdkResponseMapperTest {
sessionId = "sessionId",
identifications = listOf(),
isMultiFactorIdEnabled = false,
+ scannedCredential = null,
),
)
diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCase.kt
index 619aad1d04..9aa701fcf6 100644
--- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCase.kt
+++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCase.kt
@@ -59,7 +59,14 @@ internal class MapRefusalOrErrorResultUseCase @Inject constructor(
is ValidateSubjectPoolResult ->
result
.takeUnless { it.isValid }
- ?.let { AppIdentifyResponse(emptyList(), eventRepository.getCurrentSessionScope().id, isMultiFactorIdEnabled = false) }
+ ?.let {
+ AppIdentifyResponse(
+ identifications = emptyList(),
+ sessionId = eventRepository.getCurrentSessionScope().id,
+ isMultiFactorIdEnabled = false,
+ scannedCredential = null,
+ )
+ }
is SelectSubjectAgeGroupResult ->
result
diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCase.kt
index 0d29bebb07..da21b3ba17 100644
--- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCase.kt
+++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCase.kt
@@ -2,11 +2,13 @@ package com.simprints.feature.orchestrator.usecases.response
import com.simprints.core.domain.response.AppMatchConfidence
import com.simprints.feature.externalcredential.ExternalCredentialSearchResult
+import com.simprints.feature.externalcredential.screens.search.model.ScannedCredential
import com.simprints.infra.config.store.models.ModalitySdkType
import com.simprints.infra.config.store.models.ProjectConfiguration
import com.simprints.infra.config.store.models.getModalitySdkConfig
import com.simprints.infra.events.session.SessionEventRepository
import com.simprints.infra.matching.MatchResult
+import com.simprints.infra.orchestration.data.responses.AppExternalCredential
import com.simprints.infra.orchestration.data.responses.AppIdentifyResponse
import com.simprints.infra.orchestration.data.responses.AppMatchResult
import com.simprints.infra.orchestration.data.responses.AppResponse
@@ -23,11 +25,26 @@ internal class CreateIdentifyResponseUseCase @Inject constructor(
val isMultiFactorIdEnabled = projectConfiguration.multifactorId?.allowedExternalCredentials?.isNotEmpty() ?: false
val currentSessionId = eventRepository.getCurrentSessionScope().id
+ val externalCredential = results
+ .filterIsInstance(ExternalCredentialSearchResult::class.java)
+ .lastOrNull()
+ ?.scannedCredential
+ .toAppExternalCredential()
+
return AppIdentifyResponse(
sessionId = currentSessionId,
isMultiFactorIdEnabled = isMultiFactorIdEnabled,
// Return the results with the highest confidence score
identifications = getResults(results, projectConfiguration),
+ scannedCredential = externalCredential,
+ )
+ }
+
+ private fun ScannedCredential?.toAppExternalCredential(): AppExternalCredential? = this?.let { scannedCredential ->
+ AppExternalCredential(
+ id = scannedCredential.credentialScanId,
+ value = scannedCredential.scannedValue,
+ type = scannedCredential.credentialType,
)
}
diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/AddCallbackEventUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/AddCallbackEventUseCaseTest.kt
index 0c44669184..06e55919e9 100644
--- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/AddCallbackEventUseCaseTest.kt
+++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/AddCallbackEventUseCaseTest.kt
@@ -71,6 +71,7 @@ class AddCallbackEventUseCaseTest {
listOf(AppMatchResult("guid", 0, AppMatchConfidence.HIGH)),
"sessionId",
isMultiFactorIdEnabled = false,
+ scannedCredential = null,
),
)
diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/UpdateDailyActivityUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/UpdateDailyActivityUseCaseTest.kt
index 5d83449464..916f9d8a36 100644
--- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/UpdateDailyActivityUseCaseTest.kt
+++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/UpdateDailyActivityUseCaseTest.kt
@@ -59,7 +59,7 @@ class UpdateDailyActivityUseCaseTest {
@Test
fun `Update daily activity on identify response`() = runTest {
- useCase(AppIdentifyResponse(emptyList(), "guid", isMultiFactorIdEnabled = false))
+ useCase(AppIdentifyResponse(emptyList(), "guid", isMultiFactorIdEnabled = false, scannedCredential = null))
coVerify { recentUserActivityManager.updateRecentUserActivity(any()) }
}
diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCaseTest.kt
index 4c16737fea..3c4ceb46a3 100644
--- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCaseTest.kt
+++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCaseTest.kt
@@ -297,6 +297,7 @@ class CreateIdentifyResponseUseCaseTest {
results = listOf(
mockk {
every { matchResults } returns fingerprintMatches + faceMatches
+ every { scannedCredential } returns null
},
),
)
@@ -333,6 +334,7 @@ class CreateIdentifyResponseUseCaseTest {
results = listOf(
mockk {
every { matchResults } returns credentialFaceMatches
+ every { scannedCredential } returns null
},
MatchResult(
listOf(ComparisonResult(subjectId = sharedGuid, comparisonScore = faceConfidence)),
@@ -374,6 +376,7 @@ class CreateIdentifyResponseUseCaseTest {
results = listOf(
mockk {
every { matchResults } returns credentialFingerprintMatches
+ every { scannedCredential } returns null
},
MatchResult(
listOf(ComparisonResult(subjectId = sharedGuid, comparisonScore = fingerprintConfidence)),
@@ -444,6 +447,7 @@ class CreateIdentifyResponseUseCaseTest {
results = listOf(
mockk {
every { matchResults } returns credentialFaceMatches
+ every { scannedCredential } returns null
},
faceMatchResults,
),
diff --git a/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionResponse.kt b/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionResponse.kt
index cbb24033ad..59f746c8ff 100644
--- a/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionResponse.kt
+++ b/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionResponse.kt
@@ -25,6 +25,7 @@ sealed class ActionResponse(
override val sessionId: String,
val identifications: List,
val isMultiFactorIdEnabled: Boolean,
+ val scannedCredential: AppExternalCredential?,
) : ActionResponse(actionIdentifier, sessionId)
@ExcludedFromGeneratedTestCoverageReports("Data struct")
diff --git a/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/responses/AppIdentifyResponse.kt b/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/responses/AppIdentifyResponse.kt
index 6b22f9f81a..21019f3402 100644
--- a/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/responses/AppIdentifyResponse.kt
+++ b/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/responses/AppIdentifyResponse.kt
@@ -11,4 +11,5 @@ data class AppIdentifyResponse(
val identifications: List,
val sessionId: String,
val isMultiFactorIdEnabled: Boolean,
+ val scannedCredential: AppExternalCredential?,
) : AppResponse()
From 05bb4e6626d4f6b97b27fcb0bc73d459bd2e162b Mon Sep 17 00:00:00 2001
From: alexandr-simprints
<129499142+alexandr-simprints@users.noreply.github.com>
Date: Tue, 17 Feb 2026 13:05:32 +0000
Subject: [PATCH 7/7] [MS-1352] MFID: Accessing a correct scanned credential
field to build an Identification response (#1594)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* [MS-1352] Accessing a correct scanned credential field to build an Identification response
* [MS-1352] Adding test coverageˆ
(cherry picked from commit a5c531b878ea9b7cca7692ad30180a9b6b064ad3)
---
.../response/AppResponseBuilderUseCase.kt | 6 +-
.../response/CreateIdentifyResponseUseCase.kt | 28 +++++++--
.../response/AppResponseBuilderUseCaseTest.kt | 6 +-
.../CreateIdentifyResponseUseCaseTest.kt | 62 ++++++++++++++++++-
4 files changed, 89 insertions(+), 13 deletions(-)
diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCase.kt
index 69f2839a59..a181a66195 100644
--- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCase.kt
+++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCase.kt
@@ -29,13 +29,13 @@ internal class AppResponseBuilderUseCase @Inject constructor(
request = request,
results = results,
project = project,
- enrolmentSubjectId = enrolmentSubjectId
+ enrolmentSubjectId = enrolmentSubjectId,
)
} else {
- handleIdentify(projectConfiguration, results)
+ handleIdentify(projectConfiguration, project, results)
}
- is ActionRequest.IdentifyActionRequest -> handleIdentify(projectConfiguration, results)
+ is ActionRequest.IdentifyActionRequest -> handleIdentify(projectConfiguration, project, results)
is ActionRequest.VerifyActionRequest -> handleVerify(projectConfiguration, results)
is ActionRequest.ConfirmIdentityActionRequest -> handleConfirmIdentity(results)
is ActionRequest.EnrolLastBiometricActionRequest -> handleEnrolLastBiometric(results)
diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCase.kt
index da21b3ba17..723743f5f6 100644
--- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCase.kt
+++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCase.kt
@@ -1,11 +1,16 @@
package com.simprints.feature.orchestrator.usecases.response
import com.simprints.core.domain.response.AppMatchConfidence
+import com.simprints.core.domain.tokenization.TokenizableString
import com.simprints.feature.externalcredential.ExternalCredentialSearchResult
import com.simprints.feature.externalcredential.screens.search.model.ScannedCredential
import com.simprints.infra.config.store.models.ModalitySdkType
+import com.simprints.infra.config.store.models.DecisionPolicy
+import com.simprints.infra.config.store.models.Project
import com.simprints.infra.config.store.models.ProjectConfiguration
import com.simprints.infra.config.store.models.getModalitySdkConfig
+import com.simprints.infra.config.store.models.TokenKeyType
+import com.simprints.infra.config.store.tokenization.TokenizationProcessor
import com.simprints.infra.events.session.SessionEventRepository
import com.simprints.infra.matching.MatchResult
import com.simprints.infra.orchestration.data.responses.AppExternalCredential
@@ -17,9 +22,11 @@ import javax.inject.Inject
internal class CreateIdentifyResponseUseCase @Inject constructor(
private val eventRepository: SessionEventRepository,
+ private val tokenizationProcessor: TokenizationProcessor,
) {
suspend operator fun invoke(
projectConfiguration: ProjectConfiguration,
+ project: Project?,
results: List,
): AppResponse {
val isMultiFactorIdEnabled = projectConfiguration.multifactorId?.allowedExternalCredentials?.isNotEmpty() ?: false
@@ -29,7 +36,7 @@ internal class CreateIdentifyResponseUseCase @Inject constructor(
.filterIsInstance(ExternalCredentialSearchResult::class.java)
.lastOrNull()
?.scannedCredential
- .toAppExternalCredential()
+ ?.toAppExternalCredential(tokenizationProcessor, project)
return AppIdentifyResponse(
sessionId = currentSessionId,
@@ -40,11 +47,20 @@ internal class CreateIdentifyResponseUseCase @Inject constructor(
)
}
- private fun ScannedCredential?.toAppExternalCredential(): AppExternalCredential? = this?.let { scannedCredential ->
- AppExternalCredential(
- id = scannedCredential.credentialScanId,
- value = scannedCredential.scannedValue,
- type = scannedCredential.credentialType,
+ private fun ScannedCredential.toAppExternalCredential(
+ tokenizationProcessor: TokenizationProcessor,
+ project: Project?,
+ ): AppExternalCredential? {
+ if (project == null) return null
+ val decryptedValue = tokenizationProcessor.decrypt(
+ encrypted = credential,
+ tokenKeyType = TokenKeyType.ExternalCredential,
+ project = project,
+ ) as? TokenizableString.Raw ?: return null
+ return AppExternalCredential(
+ id = credentialScanId,
+ value = decryptedValue,
+ type = credentialType,
)
}
diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCaseTest.kt
index 1ff982ba2e..7a09848279 100644
--- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCaseTest.kt
+++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCaseTest.kt
@@ -40,7 +40,7 @@ internal class AppResponseBuilderUseCaseTest {
MockKAnnotations.init(this, relaxUnitFun = true)
coEvery { handleEnrolment.invoke(any(), any(), any(), any()) } returns mockk()
- coEvery { handleIdentify.invoke(any(), any()) } returns mockk()
+ coEvery { handleIdentify.invoke(any(), any(), any()) } returns mockk()
every { handleVerify.invoke(any(), any()) } returns mockk()
every { handleConfirmIdentity.invoke(any()) } returns mockk()
every { handleEnrolLastBiometric.invoke(any()) } returns mockk()
@@ -67,13 +67,13 @@ internal class AppResponseBuilderUseCaseTest {
fun `Handles as identification for enrolment action with existing item`() = runTest {
every { isNewEnrolment(any(), any()) } returns false
useCase(mockk(), mockk(), mockk(), mockk(), enrolmentSubjectId)
- coVerify { handleIdentify.invoke(any(), any()) }
+ coVerify { handleIdentify.invoke(any(), any(), any()) }
}
@Test
fun `Handles as identification for identification action`() = runTest {
useCase(mockk(), mockk(), mockk(), mockk(), enrolmentSubjectId)
- coVerify { handleIdentify.invoke(any(), any()) }
+ coVerify { handleIdentify.invoke(any(), any(), any()) }
}
@Test
diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCaseTest.kt
index 3c4ceb46a3..ed7dcf2465 100644
--- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCaseTest.kt
+++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCaseTest.kt
@@ -1,11 +1,16 @@
package com.simprints.feature.orchestrator.usecases.response
import com.google.common.truth.Truth.*
+import com.google.common.truth.Truth.assertThat
import com.simprints.core.domain.comparison.ComparisonResult
+import com.simprints.core.domain.externalcredential.ExternalCredentialType
+import com.simprints.core.domain.tokenization.asTokenizableRaw
import com.simprints.feature.externalcredential.ExternalCredentialSearchResult
import com.simprints.feature.externalcredential.model.CredentialMatch
import com.simprints.infra.config.store.models.DecisionPolicy
import com.simprints.infra.config.store.models.ModalitySdkType
+import com.simprints.infra.config.store.models.Project
+import com.simprints.infra.config.store.tokenization.TokenizationProcessor
import com.simprints.infra.events.session.SessionEventRepository
import com.simprints.infra.matching.MatchResult
import com.simprints.infra.orchestration.data.responses.AppIdentifyResponse
@@ -20,6 +25,12 @@ class CreateIdentifyResponseUseCaseTest {
@MockK
lateinit var eventRepository: SessionEventRepository
+ @MockK
+ lateinit var tokenizationProcessor: TokenizationProcessor
+
+ @MockK
+ lateinit var project: Project
+
private lateinit var useCase: CreateIdentifyResponseUseCase
@Before
@@ -28,7 +39,7 @@ class CreateIdentifyResponseUseCaseTest {
coEvery { eventRepository.getCurrentSessionScope().id } returns "sessionId"
- useCase = CreateIdentifyResponseUseCase(eventRepository)
+ useCase = CreateIdentifyResponseUseCase(eventRepository, tokenizationProcessor)
}
@Test
@@ -41,6 +52,7 @@ class CreateIdentifyResponseUseCaseTest {
every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns null
},
results = listOf(createFaceMatchResult(10f, 20f, 30f)),
+ project = project,
)
assertThat((result as AppIdentifyResponse).identifications).isEmpty()
@@ -56,6 +68,7 @@ class CreateIdentifyResponseUseCaseTest {
every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns null
},
results = listOf(createFaceMatchResult(10f, 20f, 30f)),
+ project = project,
)
assertThat((result as AppIdentifyResponse).identifications).isNotEmpty()
@@ -72,6 +85,7 @@ class CreateIdentifyResponseUseCaseTest {
every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns null
},
results = listOf(createFaceMatchResult(20f, 25f, 30f, 40f)),
+ project = project,
)
assertThat((result as AppIdentifyResponse).identifications).isNotEmpty()
@@ -88,6 +102,7 @@ class CreateIdentifyResponseUseCaseTest {
every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns null
},
results = listOf(createFaceMatchResult(15f, 30f, 100f)),
+ project = project,
)
assertThat((result as AppIdentifyResponse).identifications).isNotEmpty()
@@ -108,6 +123,7 @@ class CreateIdentifyResponseUseCaseTest {
)
},
results = listOf(createFingerprintMatchResult(10f, 20f, 30f)),
+ project = project,
)
assertThat((result as AppIdentifyResponse).identifications).isNotEmpty()
@@ -128,6 +144,7 @@ class CreateIdentifyResponseUseCaseTest {
)
},
results = listOf(createFingerprintMatchResult(20f, 25f, 30f, 40f)),
+ project = project,
)
assertThat((result as AppIdentifyResponse).identifications).isNotEmpty()
@@ -148,6 +165,7 @@ class CreateIdentifyResponseUseCaseTest {
)
},
results = listOf(createFingerprintMatchResult(15f, 30f, 100f)),
+ project = project,
)
assertThat((result as AppIdentifyResponse).identifications).isNotEmpty()
@@ -171,6 +189,7 @@ class CreateIdentifyResponseUseCaseTest {
createFaceMatchResult(15f, 30f, 100f),
createFingerprintMatchResult(15f, 30f, 105f),
),
+ project = project,
)
assertThat((result as AppIdentifyResponse).identifications).isNotEmpty()
@@ -194,6 +213,7 @@ class CreateIdentifyResponseUseCaseTest {
createFaceMatchResult(15f, 30f, 105f),
createFingerprintMatchResult(15f, 30f, 100f),
),
+ project = project,
)
assertThat((result as AppIdentifyResponse).identifications).isNotEmpty()
@@ -244,8 +264,10 @@ class CreateIdentifyResponseUseCaseTest {
results = listOf(
mockk {
every { matchResults } returns faceMatches + fingerprintMatches
+ every { scannedCredential } returns null
},
),
+ project = project,
)
assertThat((result as AppIdentifyResponse).identifications).isNotEmpty()
@@ -300,6 +322,7 @@ class CreateIdentifyResponseUseCaseTest {
every { scannedCredential } returns null
},
),
+ project = project,
)
assertThat((result as AppIdentifyResponse).identifications).isNotEmpty()
@@ -341,6 +364,7 @@ class CreateIdentifyResponseUseCaseTest {
ModalitySdkType.RANK_ONE,
),
),
+ project = project,
)
assertThat((result as AppIdentifyResponse).identifications).hasSize(1)
@@ -383,6 +407,7 @@ class CreateIdentifyResponseUseCaseTest {
ModalitySdkType.SECUGEN_SIM_MATCHER,
),
),
+ project = project,
)
assertThat((result as AppIdentifyResponse).identifications).hasSize(1)
@@ -451,6 +476,7 @@ class CreateIdentifyResponseUseCaseTest {
},
faceMatchResults,
),
+ project = project,
)
assertThat((result as AppIdentifyResponse).identifications).hasSize(maxNbOfReturnedCandidates)
@@ -468,6 +494,40 @@ class CreateIdentifyResponseUseCaseTest {
)
}
+ @Test
+ fun `Returns scanned credential when decryption succeeds`() = runTest {
+ val id = "id"
+ val type = ExternalCredentialType.NHISCard
+ val expectedDecrypted = "expectedDecrypted".asTokenizableRaw()
+
+ every { tokenizationProcessor.decrypt(any(), any(), any()) } returns expectedDecrypted
+
+ val result = useCase(
+ mockk {
+ every { multifactorId?.allowedExternalCredentials } returns null
+ every { identification.maxNbOfReturnedCandidates } returns 2
+ every { face?.getSdkConfiguration(any())?.decisionPolicy } returns null
+ every { fingerprint?.getSdkConfiguration(any())?.decisionPolicy } returns null
+ },
+ results = listOf(
+ mockk {
+ every { matchResults } returns emptyList()
+ every { scannedCredential } returns mockk {
+ every { credentialScanId } returns id
+ every { credentialType } returns type
+ every { credential } returns mockk()
+ }
+ },
+ ),
+ project = project,
+ )
+
+ assertThat((result as AppIdentifyResponse).scannedCredential).isNotNull()
+ assertThat(result.scannedCredential?.id).isEqualTo(id)
+ assertThat(result.scannedCredential?.type).isEqualTo(type)
+ assertThat(result.scannedCredential?.value).isEqualTo(expectedDecrypted)
+ }
+
private fun createFaceMatchResult(vararg confidences: Float): Serializable = MatchResult(
confidences.mapIndexed { i, confidence -> ComparisonResult(subjectId = "$i", comparisonScore = confidence) },
ModalitySdkType.RANK_ONE,