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/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/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/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/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) } 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..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 @@ -31,6 +33,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 @@ -58,13 +61,14 @@ 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, savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) + applySystemBarInsets(view) initObservers() } @@ -92,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 } @@ -102,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( @@ -196,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) @@ -224,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 { @@ -248,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/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..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() } @@ -42,6 +43,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 +69,9 @@ class ExternalCredentialSkipFragment : Fragment(R.layout.fragment_external_crede reasonTextInput.text.toString().isNotEmpty() } - else -> true + else -> { + true + } } buttonSkip.isEnabled = isSkipButtonEnabled } @@ -95,6 +99,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/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/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/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" /> - + + + 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/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 0d29bebb07..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,12 +1,19 @@ 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 import com.simprints.infra.orchestration.data.responses.AppIdentifyResponse import com.simprints.infra.orchestration.data.responses.AppMatchResult import com.simprints.infra.orchestration.data.responses.AppResponse @@ -15,19 +22,45 @@ 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 val currentSessionId = eventRepository.getCurrentSessionScope().id + val externalCredential = results + .filterIsInstance(ExternalCredentialSearchResult::class.java) + .lastOrNull() + ?.scannedCredential + ?.toAppExternalCredential(tokenizationProcessor, project) + return AppIdentifyResponse( sessionId = currentSessionId, isMultiFactorIdEnabled = isMultiFactorIdEnabled, // Return the results with the highest confidence score identifications = getResults(results, projectConfiguration), + scannedCredential = externalCredential, + ) + } + + 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/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/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 4c16737fea..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() @@ -297,8 +319,10 @@ class CreateIdentifyResponseUseCaseTest { results = listOf( mockk { every { matchResults } returns fingerprintMatches + faceMatches + every { scannedCredential } returns null }, ), + project = project, ) assertThat((result as AppIdentifyResponse).identifications).isNotEmpty() @@ -333,12 +357,14 @@ class CreateIdentifyResponseUseCaseTest { results = listOf( mockk { every { matchResults } returns credentialFaceMatches + every { scannedCredential } returns null }, MatchResult( listOf(ComparisonResult(subjectId = sharedGuid, comparisonScore = faceConfidence)), ModalitySdkType.RANK_ONE, ), ), + project = project, ) assertThat((result as AppIdentifyResponse).identifications).hasSize(1) @@ -374,12 +400,14 @@ class CreateIdentifyResponseUseCaseTest { results = listOf( mockk { every { matchResults } returns credentialFingerprintMatches + every { scannedCredential } returns null }, MatchResult( listOf(ComparisonResult(subjectId = sharedGuid, comparisonScore = fingerprintConfidence)), ModalitySdkType.SECUGEN_SIM_MATCHER, ), ), + project = project, ) assertThat((result as AppIdentifyResponse).identifications).hasSize(1) @@ -444,9 +472,11 @@ class CreateIdentifyResponseUseCaseTest { results = listOf( mockk { every { matchResults } returns credentialFaceMatches + every { scannedCredential } returns null }, faceMatchResults, ), + project = project, ) assertThat((result as AppIdentifyResponse).identifications).hasSize(maxNbOfReturnedCandidates) @@ -464,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, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiExternalCredentialSelectionPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiExternalCredentialSelectionPayload.kt index 75cc5f67ea..272a9689bf 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiExternalCredentialSelectionPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiExternalCredentialSelectionPayload.kt @@ -31,6 +31,7 @@ internal data class ApiExternalCredentialSelectionPayload( @Keep @Serializable enum class ApiExternalCredentialSkipReason { + HAS_NUMBER_NO_ID, DOES_NOT_HAVE_ID, DID_NOT_BRING_ID, BROUGHT_INCORRECT_ID, @@ -44,6 +45,7 @@ internal data class ApiExternalCredentialSelectionPayload( } internal fun SkipReason.toApiExternalCredentialSkipReason(): ApiExternalCredentialSkipReason = when (this) { + SkipReason.HAS_NUMBER_NO_ID -> 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/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() 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