Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
af0a0bc
[MS-1169] Initial implementation of the Enrol Last + Credential funct…
alexandr-simprints Oct 8, 2025
d16651b
[MS-1166] Initial implementation of the Skip Credential screen
alexandr-simprints Oct 8, 2025
8642b58
[MS-1166] Adding analytical reminder comment
alexandr-simprints Oct 9, 2025
261bec8
[MS-1189] Adding MFID-specific consent text
alexandr-simprints Oct 9, 2025
056e044
Merge branch 'MS-1170-mf-id-implementation-confirm-identity-credentia…
alexandr-simprints Oct 9, 2025
1b2d76c
Merge branch 'MS-1169-mf-id-implementation-enrol-last-credential-save…
alexandr-simprints Oct 9, 2025
cadde42
Merge branch 'MS-1166-mf-id-implementation-skip-screen' into MS-1189-…
alexandr-simprints Oct 9, 2025
9779b42
Merge branch 'CORE-3404-search-verify' into MS-1169-mf-id-implementat…
alexandr-simprints Oct 9, 2025
5d37b57
Merge branch 'MS-1169-mf-id-implementation-enrol-last-credential-save…
alexandr-simprints Oct 9, 2025
97da10e
Merge branch 'MS-1166-mf-id-implementation-skip-screen' into MS-1189-…
alexandr-simprints Oct 9, 2025
eaa56ba
Merge branch 'CORE-3404-search-verify' into MS-1169-mf-id-implementat…
alexandr-simprints Oct 10, 2025
86233e3
Merge branch 'MS-1169-mf-id-implementation-enrol-last-credential-save…
alexandr-simprints Oct 10, 2025
1f8510a
Merge branch 'MS-1166-mf-id-implementation-skip-screen' into MS-1189-…
alexandr-simprints Oct 10, 2025
f9d267c
[MS-1169] Updating tests
alexandr-simprints Oct 14, 2025
655b5a1
[MS-1169] Updating test coverage
alexandr-simprints Oct 15, 2025
c5e0ed1
[MS-1169] Adding extra test coverage
alexandr-simprints Oct 15, 2025
7af652b
[MS-1169] Using external credential's value field to pass to SQL buil…
alexandr-simprints Oct 15, 2025
11a28c3
Merge branch 'MS-1169-mf-id-implementation-enrol-last-credential-save…
alexandr-simprints Oct 15, 2025
927f18d
[MS-1166] Removing old tests, updating existing tests of ExternalCred…
alexandr-simprints Oct 15, 2025
74d89d7
Merge branch 'MS-1166-mf-id-implementation-skip-screen' into MS-1189-…
alexandr-simprints Oct 15, 2025
5d1e59e
[MS-1189] Adding test coverage for updated MFID consent wording
alexandr-simprints Oct 15, 2025
b562fb6
[MS-1189] Removing outdated comments
alexandr-simprints Oct 20, 2025
21193e0
Merge pull request #1409 from Simprints/MS-1189-md-id-consent-update
alexandr-simprints Oct 20, 2025
fab0972
Merge pull request #1408 from Simprints/MS-1166-mf-id-implementation-…
alexandr-simprints Oct 20, 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 @@ -115,6 +115,7 @@ class ClientApiViewModel @Inject internal constructor(
sessionId = currentSessionId,
enrolledGuid = enrolResponse.guid,
subjectActions = coSyncEnrolmentRecords,
externalCredential = enrolResponse.externalCredential,
),
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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.libsimprints.Constants
Expand Down Expand Up @@ -32,6 +33,7 @@ internal class LibSimprintsResponseMapper @Inject constructor(
Constants.SIMPRINTS_DEVICE_ID to deviceId,
Constants.SIMPRINTS_APP_VERSION_NAME to appVersionName,
Constants.SIMPRINTS_BIOMETRICS_COMPLETE_CHECK to true,
HAS_CREDENTIAL to (response.externalCredential != null),
).appendDataPerContractVersion(response) { version ->
when {
version < VersionsList.INITIAL_REWORK -> putParcelable(
Expand All @@ -42,6 +44,7 @@ internal class LibSimprintsResponseMapper @Inject constructor(
else -> putString(Constants.SIMPRINTS_ENROLMENT, Enrolment(response.enrolledGuid).toJson())
}
}.appendCoSyncData(response.subjectActions)
.appendExternalCredential(response.externalCredential)

is ActionResponse.IdentifyActionResponse -> bundleOf(
Constants.SIMPRINTS_SESSION_ID to response.sessionId,
Expand Down Expand Up @@ -73,18 +76,7 @@ internal class LibSimprintsResponseMapper @Inject constructor(
Constants.SIMPRINTS_APP_VERSION_NAME to appVersionName,
Constants.SIMPRINTS_BIOMETRICS_COMPLETE_CHECK to true,
HAS_CREDENTIAL to (response.externalCredential != null),
).also { bundle ->
val credentialJson = response.externalCredential?.let {
JSONObject()
.also {
it.put(SCANNED_CREDENTIAL_VALUE, response.externalCredential?.value)
it.put(SCANNED_CREDENTIAL_TYPE, response.externalCredential?.type)
}.toString()
}
if (credentialJson != null) {
bundle.putString(SCANNED_CREDENTIAL, credentialJson)
}
}
).appendExternalCredential(response.externalCredential)
}

is ActionResponse.VerifyActionResponse -> bundleOf(
Expand Down Expand Up @@ -160,6 +152,18 @@ internal class LibSimprintsResponseMapper @Inject constructor(
actions?.let { putString(Constants.SIMPRINTS_COSYNC_SUBJECT_ACTIONS, it) }
}

private fun Bundle.appendExternalCredential(credential: ExternalCredential?) = apply {
Comment thread
alexandr-simprints marked this conversation as resolved.
if (credential != null) {
val credentialJson =
JSONObject()
.also {
it.put(SCANNED_CREDENTIAL_VALUE, credential.value)
it.put(SCANNED_CREDENTIAL_TYPE, credential.type)
}.toString()
putString(SCANNED_CREDENTIAL, credentialJson)
}
}

private fun AppErrorReason.libSimprintsResultCode() = when (this) {
AppErrorReason.UNEXPECTED_ERROR -> Constants.SIMPRINTS_UNEXPECTED_ERROR
AppErrorReason.ROOTED_DEVICE -> Constants.SIMPRINTS_ROOTED_DEVICE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,10 @@ internal class ClientApiViewModelTest {
fun `handleEnrolResponse saves correct events`() = runTest {
viewModel.handleEnrolResponse(
mockRequest(),
mockk { every { guid } returns "guid" },
mockk {
every { guid } returns "guid"
every { externalCredential } returns null
},
)

coVerify {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class CommCareResponseMapperTest {
sessionId = "sessionId",
enrolledGuid = "guid",
subjectActions = "subjects",
externalCredential = null,
),
).getBundle(CommCareConstants.COMMCARE_BUNDLE_KEY) ?: bundleOf()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class LibSimprintsResponseMapperTest {
sessionId = "sessionId",
enrolledGuid = "guid",
subjectActions = "subjects",
externalCredential = null,
),
)

Expand All @@ -62,6 +63,7 @@ class LibSimprintsResponseMapperTest {
sessionId = "sessionId",
enrolledGuid = "guid",
subjectActions = "subjects",
externalCredential = null,
),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class OdkResponseMapperTest {
sessionId = "sessionId",
enrolledGuid = "guid",
subjectActions = "subjects",
externalCredential = null,
),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,20 +82,23 @@ internal class ConsentViewModel @Inject constructor(
selectedTabIndex: Int,
): ConsentViewState {
val allowParentalConsent = projectConfig.consent.allowParentalConsent
val isMultiFactorIdEnabled = projectConfig.multifactorId?.allowedExternalCredentials?.isNotEmpty() ?: false

return ConsentViewState(
showLogo = projectConfig.consent.displaySimprintsLogo,
showParentalConsent = allowParentalConsent,
consentTextBuilder = GeneralConsentTextHelper(
projectConfig.consent,
projectConfig.general.modalities,
consentType,
config = projectConfig.consent,
modalities = projectConfig.general.modalities,
consentType = consentType,
isMultiFactorIdEnabled = isMultiFactorIdEnabled,
),
parentalTextBuilder = if (allowParentalConsent) {
ParentalConsentTextHelper(
projectConfig.consent,
projectConfig.general.modalities,
consentType,
config = projectConfig.consent,
modalities = projectConfig.general.modalities,
consentType = consentType,
isMultiFactorIdEnabled = isMultiFactorIdEnabled,
)
} else {
null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@ internal data class GeneralConsentTextHelper(
private val config: ConsentConfiguration,
private val modalities: List<Modality>,
private val consentType: ConsentType,
private val isMultiFactorIdEnabled: Boolean,
) {
// TODO All the `getString(id).format(arg,arg)` calls should be `getString(id,arg,arg)` one strings are fixed

// First argument in consent text should always be program name, second is modality specific access/use case text
fun assembleText(context: Context) = StringBuilder()
.apply {
val modalityUseCase = getModalitySpecificUseCaseText(context, modalities)
val modalityAccess = getModalitySpecificAccessText(context, modalities)
val modalityAccess =
getModalitySpecificAccessText(context, modalities) + getMultiFactorIdAccessText(context, isMultiFactorIdEnabled)

filterAppRequestForConsent(context, consentType, config, modalityUseCase)
filterForDataSharingOptions(context, config, modalityUseCase, modalityAccess)
val requestModalityUseCase = modalityUseCase + getMultiFactorIdUseCaseText(context, isMultiFactorIdEnabled)
val dataSharingModalityUseCase = modalityUseCase + getMultiFactorIdSharingText(context, isMultiFactorIdEnabled)

filterAppRequestForConsent(context, consentType, config, requestModalityUseCase)
filterForDataSharingOptions(context, config, dataSharingModalityUseCase, modalityAccess)
}.toString()

private fun StringBuilder.filterAppRequestForConsent(
Expand Down Expand Up @@ -134,4 +138,42 @@ internal data class GeneralConsentTextHelper(
Modality.FACE -> context.getString(R.string.consent_biometrics_access_face)
Modality.FINGERPRINT -> context.getString(R.string.consent_biometrics_access_fingerprint)
}

private fun getMultiFactorIdUseCaseText(
context: Context,
isMultiFactorIdEnabled: Boolean,
): String = if (isMultiFactorIdEnabled) {
listOf(
",",
context.getString(R.string.consent_biometric_concat_modalities),
context.getString(R.string.consent_credentials_general),
).joinToString(separator = " ")
} else {
""
}

private fun getMultiFactorIdAccessText(
context: Context,
isMultiFactorIdEnabled: Boolean,
): String = if (isMultiFactorIdEnabled) {
listOf(
",",
context.getString(R.string.consent_credentials_access),
).joinToString(separator = " ")
} else {
""
}

private fun getMultiFactorIdSharingText(
context: Context,
isMultiFactorIdEnabled: Boolean,
): String = if (isMultiFactorIdEnabled) {
listOf(
",",
context.getString(R.string.consent_biometric_concat_modalities),
context.getString(R.string.consent_credentials_your_id),
).joinToString(separator = " ")
} else {
""
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,20 @@ internal data class ParentalConsentTextHelper(
private val config: ConsentConfiguration,
private val modalities: List<Modality>,
private val consentType: ConsentType,
private val isMultiFactorIdEnabled: Boolean,
) {
// TODO All the `getString(id).format(arg,arg)` calls should be `getString(id,arg,arg)` one strings are fixed

// First argument in consent text should always be program name, second is modality specific access/use case text
fun assembleText(context: Context): String = StringBuilder()
.apply {
val modalityUseCase = getModalitySpecificUseCaseText(context, modalities)
val modalityAccess = getModalitySpecificAccessText(context, modalities)
val modalityAccess =
getModalitySpecificAccessText(context, modalities) + getMultiFactorIdAccessText(context, isMultiFactorIdEnabled)

filterAppRequestForParentalConsent(context, consentType, config, modalityUseCase)
extractDataSharingOptions(context, config, modalityUseCase, modalityAccess)
val requestModalityUseCase = modalityUseCase + getMultiFactorIdUseCaseText(context, isMultiFactorIdEnabled)
val dataSharingModalityUseCase = modalityUseCase + getMultiFactorIdSharingText(context, isMultiFactorIdEnabled)
filterAppRequestForParentalConsent(context, consentType, config, requestModalityUseCase)
extractDataSharingOptions(context, config, dataSharingModalityUseCase, modalityAccess)
}.toString()

private fun StringBuilder.filterAppRequestForParentalConsent(
Expand Down Expand Up @@ -136,4 +139,42 @@ internal data class ParentalConsentTextHelper(
Modality.FINGERPRINT -> context.getString(R.string.consent_biometrics_access_fingerprint)
else -> ""
}

private fun getMultiFactorIdAccessText(
context: Context,
isMultiFactorIdEnabled: Boolean,
): String = if (isMultiFactorIdEnabled) {
listOf(
",",
context.getString(R.string.consent_credentials_parental_access),
).joinToString(separator = " ")
} else {
""
}

private fun getMultiFactorIdUseCaseText(
context: Context,
isMultiFactorIdEnabled: Boolean,
): String = if (isMultiFactorIdEnabled) {
listOf(
",",
context.getString(R.string.consent_biometric_concat_modalities),
context.getString(R.string.consent_credentials_parental_general),
).joinToString(separator = " ")
} else {
""
}

private fun getMultiFactorIdSharingText(
context: Context,
isMultiFactorIdEnabled: Boolean,
): String = if (isMultiFactorIdEnabled) {
listOf(
",",
context.getString(R.string.consent_biometric_concat_modalities),
context.getString(R.string.consent_credentials_parental_your_id),
).joinToString(separator = " ")
} else {
""
}
}
Loading