Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ class ClientApiViewModel @Inject internal constructor(
sessionId = currentSessionId,
identifications = identifyResponse.identifications,
isMultiFactorIdEnabled = identifyResponse.isMultiFactorIdEnabled,
scannedCredential = identifyResponse.scannedCredential,
),
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ internal class LibSimprintsResponseMapper @Inject constructor(
}.toJson(),
)
}
}
}.appendExternalCredential(response.scannedCredential.takeIf { response.isMultiFactorIdEnabled })

is ActionResponse.ConfirmActionResponse -> {
bundleOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ internal class ClientApiViewModelTest {
mockk {
every { identifications } returns emptyList()
every { isMultiFactorIdEnabled } returns false
every { scannedCredential } returns null
},
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class CommCareResponseMapperTest {
),
),
isMultiFactorIdEnabled = false,
scannedCredential = null,
),
)

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,6 +13,7 @@ import com.simprints.feature.clientapi.mappers.request.requestFactories.EnrolLas
import com.simprints.feature.clientapi.mappers.request.requestFactories.IdentifyRequestActionFactory
import com.simprints.feature.clientapi.mappers.request.requestFactories.VerifyActionFactory
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
Expand Down Expand Up @@ -89,6 +91,7 @@ class LibSimprintsResponseMapperTest {
),
),
isMultiFactorIdEnabled = false,
scannedCredential = null,
),
)

Expand Down Expand Up @@ -117,6 +120,7 @@ class LibSimprintsResponseMapperTest {
),
),
isMultiFactorIdEnabled = false,
scannedCredential = null,
),
)

Expand Down Expand Up @@ -467,6 +471,7 @@ class LibSimprintsResponseMapperTest {
sessionId = "sessionId",
identifications = listOf(identification1, identification2),
isMultiFactorIdEnabled = true,
scannedCredential = null,
),
)

Expand Down Expand Up @@ -496,6 +501,7 @@ class LibSimprintsResponseMapperTest {
),
),
isMultiFactorIdEnabled = true,
scannedCredential = null,
),
)

Expand Down Expand Up @@ -523,6 +529,7 @@ class LibSimprintsResponseMapperTest {
),
),
isMultiFactorIdEnabled = false,
scannedCredential = null,
),
)

Expand All @@ -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 = """{"type":"$expectedType","value":"$expectedValue"}"""
val scannedCredential = mockk<AppExternalCredential> {
every { value } returns expectedValue
every { type } returns expectedType
}

val extras = mapper(
createIdentifyActionResponse(
isMultiFactorIdEnabled = true,
scannedCredential = scannedCredential,
),
)

assertCommonMfidIdentifyFields(extras)
assertThat(extras.getString(Constants.SIMPRINTS_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<AppExternalCredential> {
every { value } returns expectedValue
every { type } returns expectedType
}

val extras = mapper(
createIdentifyActionResponse(
isMultiFactorIdEnabled = false,
scannedCredential = scannedCredential,
),
)

assertCommonMfidIdentifyFields(extras)
assertThat(extras.keySet()).doesNotContain(Constants.SIMPRINTS_SCANNED_CREDENTIAL)
}

// Helper functions
private fun createIdentifyActionResponse(
sessionId: String = "sessionId",
isMultiFactorIdEnabled: Boolean,
scannedCredential: AppExternalCredential? = null,
identifications: List<AppMatchResult> = 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\"")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class OdkResponseMapperTest {
),
),
isMultiFactorIdEnabled = false,
scannedCredential = null,
),
)

Expand All @@ -75,6 +76,7 @@ class OdkResponseMapperTest {
sessionId = "sessionId",
identifications = listOf(),
isMultiFactorIdEnabled = false,
scannedCredential = null,
),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ internal class ExternalCredentialControllerFragment : Fragment(R.layout.fragment
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)

viewModel.init(params)

internalNavController?.setGraph(R.navigation.graph_external_credential_internal)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,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
Expand All @@ -40,6 +41,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
Expand Down Expand Up @@ -98,6 +100,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
Expand Down Expand Up @@ -211,6 +216,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()))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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$")
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand All @@ -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(
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
}
Loading
Loading