Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
f063852
[MS-1198] Identification response now includes credential information…
alexandr-simprints Oct 9, 2025
514ee12
Merge branch 'MS-1189-md-id-consent-update' into MS-1198-mf-id-implem…
alexandr-simprints Oct 9, 2025
741d69d
Merge branch 'MS-1189-md-id-consent-update' into MS-1198-mf-id-implem…
alexandr-simprints Oct 10, 2025
6c97652
[MS-1199] Adding 'EnrolmentUpdate' event that captures addition of ex…
alexandr-simprints Oct 12, 2025
dd94835
[MFID PREVIEW] Removing tests from the build actions. Leaving only St…
alexandr-simprints Oct 12, 2025
9b5db4d
[MFID PREVIEW] Passing external credential value instead of tokenizat…
alexandr-simprints Oct 13, 2025
a78a48b
[MFID PREVIEW] Removing legacy response types for the MFID testing
alexandr-simprints Oct 13, 2025
447dcf3
[MFID PREVIEW] Verification threshold is now explicitly passed to the…
alexandr-simprints Oct 13, 2025
e58b44f
[MFID PREVIEW] External credential field is now an array in the ApiEn…
alexandr-simprints Oct 13, 2025
ea63685
[MFID PREVIEW BUILD] Creating 'AppExternalCredential' class for app r…
alexandr-simprints Oct 14, 2025
20d8d0d
[MFID PREVIEW BUILD] Passing payload ID for credential, instead of cr…
alexandr-simprints Oct 14, 2025
b4af02e
[MFID PREVIEW BUILD] Enrolment Record update now uses list of credent…
alexandr-simprints Oct 14, 2025
a4958da
[MFID PREVIEW BUILD] Adding extra logging to be able to trace down th…
alexandr-simprints Oct 14, 2025
de9d99e
[MFID PREVIEW BUILD] Removing extra logging that traced the external …
alexandr-simprints Oct 15, 2025
52a2601
Merge branch 'MS-1189-md-id-consent-update' into MS-1198-mf-id-implem…
alexandr-simprints Oct 15, 2025
6cda049
Merge branch 'MS-1198-mf-id-implementation-update-identification-and-…
alexandr-simprints Oct 15, 2025
14c5ca5
Merge branch 'MS-1199-mf-id-implementation-fix-sync-issues' into mfid…
alexandr-simprints Oct 15, 2025
87f4b6e
[MFID PREVIEW] Fixing tests
alexandr-simprints Oct 16, 2025
23b7584
[MFID PREVIEW] Fixing tests
alexandr-simprints Oct 16, 2025
0241ddd
[MFID PREVIEW] Removing unnecessary plurals from strings.xml, and ren…
alexandr-simprints Oct 20, 2025
245c173
[MFID PREVIEW] Changing plural string to a singular
alexandr-simprints Oct 20, 2025
5e874b6
Merge branch 'CORE-3404-search-verify' into mfid-preview-build-2025-4-0
alexandr-simprints Oct 20, 2025
3129f96
[MFID PREVIEW] Adding test coverage for LibSimprintsResponseMapper
alexandr-simprints Oct 21, 2025
42f4b35
[MFID PREVIEW] Adding test coverage for ExternalCredentialViewModel
alexandr-simprints Oct 21, 2025
816a0ec
[MFID PREVIEW] Adding test coverage for ClientApiViewModel
alexandr-simprints Oct 21, 2025
86df903
[MFID PREVIEW] Excluding events from test coverage reports
alexandr-simprints Oct 21, 2025
8ee8efc
Merge branch 'CORE-3404-search-verify' into mfid-preview-build-2025-4-0
alexandr-simprints Oct 21, 2025
053e62b
[MFID PREVIEW] Reverting deployment workflows that were created speci…
alexandr-simprints Oct 21, 2025
577f433
MS-1205 Remove redundant endTime from credential capture event payload
luhmirin-s Oct 20, 2025
7b9333e
MS-1205 Add boilerplate and track ExternalCredentialSearch event
luhmirin-s Oct 20, 2025
8aac815
MS-1205 Add correct values to ExternalCredentialCaptureEvent
luhmirin-s Oct 21, 2025
f506e29
MS-1205 Add ExternalCredentialSelection tracking with boilerplate
luhmirin-s Oct 21, 2025
59c71d0
MS-1205 Add ExternalCredentialConfirmationEvent tracking and boilerplate
luhmirin-s Oct 21, 2025
989fb80
Merge pull request #1420 from Simprints/feature/MS-1205-mfid-events-1
luhmirin-s Oct 22, 2025
e8b6f84
Translate strings.xml in am
transifex-integration[bot] Oct 21, 2025
42e9545
Translate strings.xml in fr
transifex-integration[bot] Oct 22, 2025
ab69457
Translate strings.xml in hi
transifex-integration[bot] Oct 22, 2025
c6bd0f6
Translate strings.xml in bn
transifex-integration[bot] Oct 22, 2025
44d9ee1
Remove am-ET translations
luhmirin-s Oct 22, 2025
bec2e5a
Merge branch 'CORE-3404-search-verify' into mfid-preview-build-2025-4-0
alexandr-simprints Oct 22, 2025
06822c8
Merge branch 'CORE-3404-search-verify' into mfid-preview-build-2025-4-0
alexandr-simprints Oct 22, 2025
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 @@ -5,6 +5,8 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.simprints.core.domain.externalcredential.ExternalCredential
import com.simprints.core.domain.tokenization.TokenizableString
import com.simprints.core.livedata.LiveDataEvent
import com.simprints.core.livedata.LiveDataEventWithContent
import com.simprints.core.livedata.send
Expand All @@ -21,6 +23,9 @@ import com.simprints.feature.clientapi.usecases.GetEnrolmentCreationEventForSubj
import com.simprints.feature.clientapi.usecases.IsFlowCompletedWithErrorUseCase
import com.simprints.feature.clientapi.usecases.SimpleEventReporter
import com.simprints.infra.authstore.AuthStore
import com.simprints.infra.config.store.models.Project
import com.simprints.infra.config.store.models.TokenKeyType
import com.simprints.infra.config.store.tokenization.TokenizationProcessor
import com.simprints.infra.config.sync.ConfigManager
import com.simprints.infra.logging.LoggingConstants.CrashReportTag.ORCHESTRATION
import com.simprints.infra.logging.Simber
Expand All @@ -30,6 +35,7 @@ import com.simprints.infra.orchestration.data.ActionResponse
import com.simprints.infra.orchestration.data.responses.AppConfirmationResponse
import com.simprints.infra.orchestration.data.responses.AppEnrolResponse
import com.simprints.infra.orchestration.data.responses.AppErrorResponse
import com.simprints.infra.orchestration.data.responses.AppExternalCredential
import com.simprints.infra.orchestration.data.responses.AppIdentifyResponse
import com.simprints.infra.orchestration.data.responses.AppRefusalResponse
import com.simprints.infra.orchestration.data.responses.AppVerifyResponse
Expand All @@ -53,6 +59,7 @@ class ClientApiViewModel @Inject internal constructor(
private val configManager: ConfigManager,
private val timeHelper: TimeHelper,
private val persistentLogger: PersistentLogger,
private val tokenizationProcessor: TokenizationProcessor,
) : ViewModel() {
val returnResponse: LiveData<LiveDataEventWithContent<Bundle>>
get() = _returnResponse
Expand Down Expand Up @@ -115,7 +122,7 @@ class ClientApiViewModel @Inject internal constructor(
sessionId = currentSessionId,
enrolledGuid = enrolResponse.guid,
subjectActions = coSyncEnrolmentRecords,
externalCredential = enrolResponse.externalCredential,
externalCredential = enrolResponse.externalCredential?.toAppExternalCredential(tokenizationProcessor, getProject()),
),
),
)
Expand All @@ -140,6 +147,7 @@ class ClientApiViewModel @Inject internal constructor(
actionIdentifier = action.actionIdentifier,
sessionId = currentSessionId,
identifications = identifyResponse.identifications,
isMultiFactorIdEnabled = identifyResponse.isMultiFactorIdEnabled,
),
),
)
Expand All @@ -162,7 +170,7 @@ class ClientApiViewModel @Inject internal constructor(
actionIdentifier = action.actionIdentifier,
sessionId = currentSessionId,
confirmed = confirmResponse.identificationOutcome,
externalCredential = confirmResponse.externalCredential,
externalCredential = confirmResponse.externalCredential?.toAppExternalCredential(tokenizationProcessor, getProject()),
),
),
)
Expand Down Expand Up @@ -264,4 +272,21 @@ class ClientApiViewModel @Inject internal constructor(
body = "${action.actionIdentifier}\n$response",
)
}

private fun ExternalCredential.toAppExternalCredential(
tokenizationProcessor: TokenizationProcessor,
project: Project?,
): AppExternalCredential? {
if (project == null) return null
val decryptedValue = tokenizationProcessor.decrypt(
encrypted = value,
tokenKeyType = TokenKeyType.ExternalCredential,
project = project,
) as? TokenizableString.Raw ?: return null
return AppExternalCredential(
id = id,
value = decryptedValue,
type = type,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import android.os.Bundle
import androidx.core.os.bundleOf
import com.simprints.core.DeviceID
import com.simprints.core.PackageVersionName
import com.simprints.core.domain.externalcredential.ExternalCredential
import com.simprints.core.domain.response.AppErrorReason
import com.simprints.infra.orchestration.data.ActionResponse
import com.simprints.infra.orchestration.data.responses.AppExternalCredential
import com.simprints.libsimprints.Constants
import com.simprints.libsimprints.contracts.VersionsList
import com.simprints.libsimprints.contracts.data.ConfidenceBand
Expand All @@ -15,6 +15,7 @@ import com.simprints.libsimprints.contracts.data.Identification
import com.simprints.libsimprints.contracts.data.Identification.Companion.toJson
import com.simprints.libsimprints.contracts.data.RefusalForm
import com.simprints.libsimprints.contracts.data.Verification
import org.json.JSONArray
import org.json.JSONObject
import javax.inject.Inject
import com.simprints.libsimprints.Identification as LegacyIdentification
Expand Down Expand Up @@ -53,6 +54,11 @@ internal class LibSimprintsResponseMapper @Inject constructor(
Constants.SIMPRINTS_BIOMETRICS_COMPLETE_CHECK to true,
).appendDataPerContractVersion(response) { version ->
when {
response.isMultiFactorIdEnabled -> putString(
Constants.SIMPRINTS_IDENTIFICATIONS,
response.mapIdentificationsWithCredentials(),
)

version < VersionsList.INITIAL_REWORK -> putParcelableArrayList(
Constants.SIMPRINTS_IDENTIFICATIONS,
response.identifications
Expand Down Expand Up @@ -152,7 +158,7 @@ internal class LibSimprintsResponseMapper @Inject constructor(
actions?.let { putString(Constants.SIMPRINTS_COSYNC_SUBJECT_ACTIONS, it) }
}

private fun Bundle.appendExternalCredential(credential: ExternalCredential?) = apply {
private fun Bundle.appendExternalCredential(credential: AppExternalCredential?) = apply {
if (credential != null) {
val credentialJson =
JSONObject()
Expand All @@ -164,6 +170,21 @@ internal class LibSimprintsResponseMapper @Inject constructor(
}
}

private fun ActionResponse.IdentifyActionResponse.mapIdentificationsWithCredentials(): String = identifications
.map { identification ->
JSONObject()
.also { json ->
json.put(KEY_GUID, identification.guid)
json.put(KEY_CONFIDENCE_BAND, identification.matchConfidence.name)
json.put(KEY_CONFIDENCE, identification.confidenceScore.toFloat())
json.put(KEY_IS_LINKED_TO_CREDENTIAL, identification.isLinkedToScannedCredential ?: false)
identification.isCredentialVerified?.let {
json.put(KEY_IS_CREDENTIAL_VERIFIED, it)
}
}
}.run(::JSONArray)
.toString()

private fun AppErrorReason.libSimprintsResultCode() = when (this) {
AppErrorReason.UNEXPECTED_ERROR -> Constants.SIMPRINTS_UNEXPECTED_ERROR
AppErrorReason.ROOTED_DEVICE -> Constants.SIMPRINTS_ROOTED_DEVICE
Expand Down Expand Up @@ -204,5 +225,12 @@ internal class LibSimprintsResponseMapper @Inject constructor(
internal const val SCANNED_CREDENTIAL = "scannedCredential"
internal const val SCANNED_CREDENTIAL_VALUE = "value"
internal const val SCANNED_CREDENTIAL_TYPE = "type"

// TODO [MS-1190] Move implementation to LibSimprints. These constats are copies of com.simprints.libsimprints.contracts.data.Identification
private const val KEY_GUID = "guid"
private const val KEY_CONFIDENCE = "confidence"
private const val KEY_CONFIDENCE_BAND = "confidenceBand"
private const val KEY_IS_LINKED_TO_CREDENTIAL = "isLinkedToCredential"
private const val KEY_IS_CREDENTIAL_VERIFIED = "isVerified"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ internal class GetEnrolmentCreationEventForSubjectUseCase @Inject constructor(
?.fromSubjectToEnrolmentCreationEvent()

if (recordCreationEvent == null) {
Simber.e("Couldn't find enrolment for subjectActions", IllegalStateException("No enrolment record found for subjectId: $subjectId"))
Simber.e(
"Couldn't find enrolment for subjectActions",
IllegalStateException("No enrolment record found for subjectId: $subjectId"),
)
return null
}

Expand All @@ -52,8 +55,7 @@ internal class GetEnrolmentCreationEventForSubjectUseCase @Inject constructor(
moduleId = moduleId,
attendantId = attendantId,
biometricReferences = EnrolmentRecordCreationEvent.buildBiometricReferences(fingerprintSamples, faceSamples, encoder),
// TODO [CORE-3421] Review if EnrolmentRecordCreationEvent should contain List of external credentials, as it currently doesn't make sense
externalCredentials = externalCredentials
externalCredentials = externalCredentials,
)

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import android.os.Bundle
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.ext.junit.runners.*
import com.jraska.livedata.test
import com.simprints.core.domain.externalcredential.ExternalCredential
import com.simprints.core.domain.externalcredential.ExternalCredentialType
import com.simprints.core.domain.tokenization.TokenizableString
import com.simprints.core.domain.tokenization.asTokenizableRaw
import com.simprints.core.tools.time.TimeHelper
import com.simprints.core.tools.time.Timestamp
import com.simprints.feature.clientapi.exceptions.InvalidRequestException
Expand All @@ -16,10 +20,14 @@ import com.simprints.feature.clientapi.usecases.GetEnrolmentCreationEventForSubj
import com.simprints.feature.clientapi.usecases.IsFlowCompletedWithErrorUseCase
import com.simprints.feature.clientapi.usecases.SimpleEventReporter
import com.simprints.infra.authstore.AuthStore
import com.simprints.infra.config.store.models.Project
import com.simprints.infra.config.store.models.TokenKeyType
import com.simprints.infra.config.store.tokenization.TokenizationProcessor
import com.simprints.infra.config.sync.ConfigManager
import com.simprints.infra.orchestration.data.ActionRequest
import com.simprints.infra.orchestration.data.ActionRequestIdentifier
import com.simprints.infra.orchestration.data.ActionResponse
import com.simprints.infra.orchestration.data.responses.AppEnrolResponse
import com.simprints.logging.persistent.PersistentLogger
import com.simprints.testtools.common.coroutines.TestCoroutineRule
import io.mockk.*
Expand Down Expand Up @@ -74,6 +82,9 @@ internal class ClientApiViewModelTest {
@MockK
lateinit var persistentLogger: PersistentLogger

@MockK
lateinit var tokenizationProcessor: TokenizationProcessor

private lateinit var viewModel: ClientApiViewModel

@Before
Expand Down Expand Up @@ -101,6 +112,7 @@ internal class ClientApiViewModelTest {
configManager = configManager,
timeHelper = timeHelper,
persistentLogger = persistentLogger,
tokenizationProcessor = tokenizationProcessor,
)
}

Expand Down Expand Up @@ -162,7 +174,10 @@ internal class ClientApiViewModelTest {
fun `handleIdentifyResponse saves correct events`() = runTest {
viewModel.handleIdentifyResponse(
mockRequest(),
mockk { every { identifications } returns emptyList() },
mockk {
every { identifications } returns emptyList()
every { isMultiFactorIdEnabled } returns false
},
)

coVerify {
Expand Down Expand Up @@ -246,6 +261,80 @@ internal class ClientApiViewModelTest {
viewModel.returnResponse.test().assertHasValue()
}

@Test
fun `handleEnrolResponse with externalCredential decrypts and includes it in response`() = runTest {
val mockGuid = "mockGuid"
val expectedCredentialId = "credentialId"
val expectedType = ExternalCredentialType.NHISCard
val credential = mockExternalCredential(expectedCredentialId, expectedType)
val project = mockk<Project>(relaxed = true)
setupDecryption(project, "decrypted-value".asTokenizableRaw())

viewModel.handleEnrolResponse(mockRequest(), mockEnrolResponseWithCredential(mockGuid, credential))

verify {
resultMapper.invoke(
match<ActionResponse.EnrolActionResponse> {
it.externalCredential?.id == expectedCredentialId &&
it.externalCredential?.type == expectedType
},
)
}
}

@Test
fun `handleEnrolResponse with externalCredential but encrypted decryption returns null credential`() = runTest {
val mockGuid = "mockGuid"
val expectedCredentialId = "credentialId"
val expectedType = ExternalCredentialType.NHISCard
val credential = mockExternalCredential(expectedCredentialId, expectedType)
val project = mockk<Project>(relaxed = true)
setupDecryption(project, mockk<TokenizableString.Tokenized>())

viewModel.handleEnrolResponse(mockRequest(), mockEnrolResponseWithCredential(mockGuid, credential))

verify {
resultMapper.invoke(
match<ActionResponse.EnrolActionResponse> {
it.externalCredential == null
},
)
}
}

private fun mockEnrolResponseWithCredential(
mockGuid: String,
credential: ExternalCredential?,
): AppEnrolResponse = mockk {
every { guid } returns mockGuid
every { externalCredential } returns credential
}

private fun mockExternalCredential(
mockId: String,
mockType: ExternalCredentialType,
): ExternalCredential = mockk {
every { id } returns mockId
every { value } returns mockk()
every { type } returns mockType
}

private fun setupDecryption(
project: Project,
returnValue: TokenizableString,
) {
val projectId = "projectId"
every { authStore.signedInProjectId } returns projectId
coEvery { configManager.getProject(projectId) } returns project
every {
tokenizationProcessor.decrypt(
encrypted = any(),
tokenKeyType = TokenKeyType.ExternalCredential,
project = project,
)
} returns returnValue
}

private fun mockRequest(): ActionRequest = mockk {
every { projectId } returns "projectId"
every { actionIdentifier } returns ActionRequestIdentifier("action", "package", "", 1, 0L)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class CommCareResponseMapperTest {
matchConfidence = AppMatchConfidence.LOW,
),
),
isMultiFactorIdEnabled = false,
),
)

Expand Down
Loading