Skip to content

Commit

Permalink
Base Card, PDF417 and MRZ Scanners (#22)
Browse files Browse the repository at this point in the history
- generic multi-credential base card (+ credential pack)
- a generic PDF417 scanner
- an initial MRZ (OCR) scanner based on the first QR Code scanner implementation
- refactors the previous QR Code Scanner version

Co-authored-by: Gregório Granado Magalhães <[email protected]>
  • Loading branch information
Juliano1612 and w4ll3 authored Aug 28, 2024
1 parent 6056c61 commit 0631c88
Show file tree
Hide file tree
Showing 11 changed files with 1,232 additions and 306 deletions.
6 changes: 3 additions & 3 deletions MobileSdk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ android {
}

dependencies {
api("com.spruceid.mobile.sdk.rs:mobilesdkrs:0.0.26")
api("com.spruceid.mobile.sdk.rs:mobilesdkrs:0.0.28")
//noinspection GradleCompatible
implementation("com.android.support:appcompat-v7:28.0.0")
/* Begin UI dependencies */
Expand All @@ -128,10 +128,10 @@ dependencies {
implementation("androidx.camera:camera-view:1.3.2")
implementation("com.google.zxing:core:3.5.1")
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
implementation("androidx.test.ext:junit-ktx:1.1.5")
implementation("androidx.camera:camera-mlkit-vision:1.3.0-alpha06")
implementation("com.google.android.gms:play-services-mlkit-text-recognition:19.0.0")
/* End UI dependencies */
testImplementation("junit:junit:4.13.2")

androidTestImplementation("com.android.support.test:runner:1.0.2")
androidTestImplementation("com.android.support.test.espresso:espresso-core:3.0.2")
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
package com.spruceid.mobile.sdk

open class BaseCredential constructor(private val id: String?) {
open class BaseCredential {
private var id: String?

constructor() {
this.id = null
}

constructor(id: String) {
this.id = id
}

fun getId(): String? {
return this.id
}

fun setId(id: String) {
this.id = id
}

override fun toString(): String {
return "Credential($id)"
}

open fun get(keys: List<String>): Map<String, Any> {
return if (keys.contains("id")) {
mapOf("id" to this.id!!)
} else {
emptyMap()
}
}
}
108 changes: 108 additions & 0 deletions MobileSdk/src/main/java/com/spruceid/mobile/sdk/CredentialPack.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.spruceid.mobile.sdk

import java.security.KeyFactory
import java.security.KeyStore
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.spec.PKCS8EncodedKeySpec
import java.util.Base64

/**
* Collection of BaseCredentials with methods to interact with all instances
*/
class CredentialPack {
private val credentials: MutableList<BaseCredential>

constructor() {
credentials = mutableListOf()
}

constructor(credentialsArray: MutableList<BaseCredential>) {
this.credentials = credentialsArray
}

fun addW3CVC(credentialString: String): List<BaseCredential> {
val vc = W3CVC(credentialString = credentialString)
credentials.add(vc)
return credentials
}

fun addMDoc(
id: String,
mdocBase64: String,
keyPEM: String,
keyBase64: String
): List<BaseCredential> {
try {
val decodedKey = Base64.getDecoder().decode(
keyBase64
)

val privateKey = KeyFactory.getInstance(
"EC"
).generatePrivate(
PKCS8EncodedKeySpec(
decodedKey
)
)

val cert: Array<Certificate> = arrayOf(
CertificateFactory.getInstance(
"X.509"
).generateCertificate(
keyPEM.byteInputStream()
)
)

val ks: KeyStore = KeyStore.getInstance(
"AndroidKeyStore"
)

ks.load(
null
)

ks.setKeyEntry(
"someAlias",
privateKey,
null,
cert
)

credentials.add(
MDoc(
id,
Base64.getDecoder().decode(mdocBase64),
"someAlias"
)
)
} catch (e: Throwable) {
print(
e
)
throw e
}
return credentials
}

fun get(keys: List<String>): Map<String, Map<String, Any>> {
val values = emptyMap<String, Map<String, Any>>().toMutableMap()

for (credential in credentials) {
values[credential.getId()!!] = credential.get(keys)
}
return values
}

fun getCredentialsByIds(credentialsIds: List<String>): List<BaseCredential> {
return credentials.filter { credential -> credentialsIds.contains(credential.getId()) }
}

fun getCredentials(): List<BaseCredential> {
return credentials
}

fun getCredentialById(credentialId: String): BaseCredential? {
return credentials.find { credential -> credential.getId().equals(credentialId) }
}
}
34 changes: 34 additions & 0 deletions MobileSdk/src/main/java/com/spruceid/mobile/sdk/W3CVC.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.spruceid.mobile.sdk

import org.json.JSONObject

class W3CVC(credentialString: String): BaseCredential() {
private var credential: JSONObject = JSONObject(credentialString)

init {
super.setId(credential.getString("id"))
}

override fun get(keys: List<String>): Map<String, Any> {
val res = mutableMapOf<String,Any>()

for (key in keys) {
res[key] = keyPathFinder(credential, key.split(".").toMutableList())
}
return res
}

private fun keyPathFinder(json: Any, path: MutableList<String>): Any {
try {
val firstKey = path.first()
val element = (json as JSONObject)[firstKey]
path.removeAt(0)
if (path.isNotEmpty()) {
return keyPathFinder(element, path)
}
return element
} catch (e: Exception) {
return ""
}
}
}
173 changes: 173 additions & 0 deletions MobileSdk/src/main/java/com/spruceid/mobile/sdk/ui/BaseCard.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package com.spruceid.mobile.sdk.ui

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.spruceid.mobile.sdk.CredentialPack

/**
* Data class with the specification to display the credential pack in a list view
* @property titleKeys A list of keys that will be used to generate a list of values extracted from the credentials
* @property titleFormatter Method used to create a custom title field. Receives an array of values based on the array of keys for the same field
* @property descriptionKeys A list of keys that will be used to generate a list of values extracted from the credentials
* @property descriptionFormatter Method used to create a custom description field. Receives an array of values based on the array of keys for the same field
* @property leadingIconKeys A list of keys that will be used to generate a list of values extracted from the credentials
* @property leadingIconFormatter Method used to create a custom leading icon formatter. Receives an array of values based on the array of keys for the same field
* @property trailingActionKeys A list of keys that will be used to generate a list of values extracted from the credentials
* @property trailingActionButton Method used to create a custom trailing action button. Receives an array of values based on the array of keys for the same field
*/
data class CardRenderingListView(
val titleKeys: List<String>,
val titleFormatter: @Composable ((values: Map<String, Map<String, Any>>) -> Unit)? = null,
val descriptionKeys: List<String>? = null,
val descriptionFormatter: @Composable ((values: Map<String, Map<String, Any>>) -> Unit)? = null,
val leadingIconKeys: List<String>? = null,
val leadingIconFormatter: @Composable ((values: Map<String, Map<String, Any>>) -> Unit)? = null,
val trailingActionKeys: List<String>? = null,
val trailingActionButton: @Composable ((values: Map<String, Map<String, Any>>) -> Unit)? = null
)

/**
* Data class with the specification to display the credential field in a details view
* @property keys A list of keys that will be used to generate a list of values extracted from the credentials
* @property formatter Method used to create a custom field. Receives an array of values based on the array of keys for the same field
*/
data class CardRenderingDetailsField(
val keys: List<String>,
val formatter: @Composable ((values: Map<String, Map<String, Any>>) -> Unit)? = null
)

/**
* Data class with the specification to display the credential in a details view
* @property fields A list of field render settings that will be used to generate a UI element with the defined keys
*/
data class CardRenderingDetailsView(
val fields: List<CardRenderingDetailsField>
)


/**
* Interface aggregating two types:
* (LIST == CardRenderingListView) and
* (DETAILS == CardRenderingDetailsView)
*/
sealed interface CardRendering
@JvmInline
value class LIST(val rendering: CardRenderingListView) : CardRendering
@JvmInline
value class DETAILS(val rendering: CardRenderingDetailsView) : CardRendering

/**
* Method to convert CardRenderingListView to CardRendering
*/
fun CardRenderingListView.toCardRendering() = LIST(this)
/**
* Method to convert CardRenderingDetailsView to CardRendering
*/
fun CardRenderingDetailsView.toCardRendering() = DETAILS(this)

/**
* Manages the card rendering type according with the render object
* @property credentialPack CredentialPack instance
* @property rendering CardRendering instance
*/
@Composable
fun BaseCard(
credentialPack: CredentialPack,
rendering: CardRendering
) {
when(rendering) {
is LIST ->
CardListView(credentialPack = credentialPack, rendering = rendering.rendering)
is DETAILS ->
CardDetailsView(credentialPack = credentialPack, rendering = rendering.rendering)
}
}

/**
* Renders the credential as a list view item
* @property credentialPack CredentialPack instance
* @property rendering CardRenderingListView instance
*/
@Composable
fun CardListView(
credentialPack: CredentialPack,
rendering: CardRenderingListView
) {
val titleValues = credentialPack.get(rendering.titleKeys)
val descriptionValues = credentialPack.get(rendering.descriptionKeys ?: emptyList())

Row(
Modifier.height(intrinsicSize = IntrinsicSize.Max)
) {
// Leading icon
if(rendering.leadingIconFormatter != null) {
rendering.leadingIconFormatter.invoke(
credentialPack.get(rendering.leadingIconKeys ?: emptyList())
)
}

Column {
// Title
if(rendering.titleFormatter != null) {
rendering.titleFormatter.invoke(titleValues)
} else {
Text(text = titleValues.values
.fold(emptyList<String>()) { acc, next -> acc + next.values
.joinToString(" ") { value -> value.toString() }
}.joinToString("").trim())
}

// Description
if(rendering.descriptionFormatter != null) {
rendering.descriptionFormatter.invoke(descriptionValues)
} else {
Text(text = descriptionValues.values
.fold(emptyList<String>()) { acc, next -> acc + next.values
.joinToString(" ") { value -> value.toString() }
}.joinToString("").trim())
}
}

Spacer(modifier = Modifier.weight(1.0f))

// Trailing action button
if(rendering.trailingActionButton != null) {
rendering.trailingActionButton.invoke(
credentialPack.get(rendering.trailingActionKeys ?: emptyList())
)
}
}
}

/**
* Renders the credential as a details view
* @property credentialPack CredentialPack instance
* @property rendering CardRenderingDetailsView instance
*/
@Composable
fun CardDetailsView(
credentialPack: CredentialPack,
rendering: CardRenderingDetailsView
) {
Column {
rendering.fields.forEach {
val values = credentialPack.get(it.keys)

if(it.formatter != null) {
it.formatter.invoke(values)
} else {
Text(text = values.values
.fold(emptyList<String>()) { acc, next -> acc + next.values
.joinToString(" ") { value -> value.toString() }
}.joinToString("").trim())
}
}
}

}
Loading

0 comments on commit 0631c88

Please sign in to comment.