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
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let package = Package(
.package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0")
],
targets: [
.binaryTarget(name: "RustFramework", url: "https://github.com/spruceid/sprucekit-mobile/releases/download/0.14.2/RustFramework.xcframework.zip", checksum: "994bfeb98e8e65da1c8fbdcf01d2293a866c46504d9d111cfb402bcccae362a4"),
.binaryTarget(name: "RustFramework", path: "rust/MobileSdkRs/RustFramework.xcframework"),
.target(
name: "SpruceIDMobileSdkRs",
dependencies: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.spruceid.mobile.sdk.rs.Cwt
import com.spruceid.mobile.sdk.rs.JsonVc
import com.spruceid.mobile.sdk.rs.JwtVc
import com.spruceid.mobile.sdk.rs.Mdoc
import com.spruceid.mobile.sdk.rs.IetfSdJwtVc
import com.spruceid.mobile.sdk.rs.Vcdm2SdJwt
import org.json.JSONException
import org.json.JSONObject
Expand Down Expand Up @@ -149,6 +150,30 @@ fun Vcdm2SdJwt.credentialClaimsFiltered(claimNames: List<String>): JSONObject {
return new
}

/**
* Access the IETF SD-JWT VC credential (dc+sd-jwt).
*/
fun IetfSdJwtVc.credentialClaims(): JSONObject {
try {
return JSONObject(this.revealedClaimsAsJsonString())
} catch (e: Error) {
print("failed to decode dc+sd-jwt data from UTF-8-encoded JSON")
return JSONObject()
}
}

/**
* Access the specified claims from the IETF SD-JWT VC credential (dc+sd-jwt).
*/
fun IetfSdJwtVc.credentialClaimsFiltered(claimNames: List<String>): JSONObject {
val old = this.credentialClaims()
val new = JSONObject()
for (name in claimNames) {
new.put(name, keyPathFinder(old, name.split(".").toMutableList()))
}
return new
}

private fun keyPathFinder(json: Any, path: MutableList<String>): Any {
try {
val firstKey = path.first()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.spruceid.mobile.sdk
import android.util.Log
import com.spruceid.mobile.sdk.rs.CredentialDecodingException
import com.spruceid.mobile.sdk.rs.Cwt
import com.spruceid.mobile.sdk.rs.IetfSdJwtVc
import com.spruceid.mobile.sdk.rs.IssuanceServiceClient
import com.spruceid.mobile.sdk.rs.JsonVc
import com.spruceid.mobile.sdk.rs.JwtVc
Expand Down Expand Up @@ -61,6 +62,11 @@ class CredentialPack {
} catch (_: Exception) {
}

try {
return this.addDcSdJwt(IetfSdJwtVc.newFromCompactSdJwt(rawCredential))
} catch (_: Exception) {
}

try {
return this.addJwtVc(JwtVc.newFromCompactJws(rawCredential))
} catch (_: Exception) {
Expand Down Expand Up @@ -165,6 +171,14 @@ class CredentialPack {
return credentials
}

/**
* Add an IETF SD-JWT VC (dc+sd-jwt) to the CredentialPack.
*/
fun addDcSdJwt(dcSdJwt: IetfSdJwtVc): List<ParsedCredential> {
credentials.add(ParsedCredential.newDcSdJwt(dcSdJwt))
return credentials
}

/**
* Add a CWT to the CredentialPack.
*/
Expand Down Expand Up @@ -232,6 +246,9 @@ class CredentialPack {
res[credentialId] = CredentialStatusList.UNDEFINED
}
}
credential.asDcSdJwt()?.let {
res[credentialId] = CredentialStatusList.UNDEFINED
}
}
return res
}
Expand All @@ -255,6 +272,7 @@ class CredentialPack {
val jwtVc = credential.asJwtVc()
val jsonVc = credential.asJsonVc()
val sdJwt = credential.asSdJwt()
val dcSdJwt = credential.asDcSdJwt()
val cwt = credential.asCwt()

if (mdoc != null) {
Expand Down Expand Up @@ -287,6 +305,12 @@ class CredentialPack {
} else {
sdJwt.credentialClaims()
}
} else if (dcSdJwt != null) {
claims = if (claimNames.isNotEmpty()) {
dcSdJwt.credentialClaimsFiltered(claimNames)
} else {
dcSdJwt.credentialClaims()
}
} else {
var type: String
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ fun GenericCredentialItemDetails(
cred?.asJsonVc() != null ||
cred?.asSdJwt() != null ||
cred?.asMsoMdoc() != null ||
cred?.asDcSdJwt() != null ||
cred?.asCwt() != null
) {
it.second
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import com.spruceid.mobilesdkexample.ui.theme.ColorBase300
import com.spruceid.mobilesdkexample.ui.theme.ColorStone600
import com.spruceid.mobilesdkexample.ui.theme.ColorStone950
import com.spruceid.mobilesdkexample.ui.theme.Inter
import com.spruceid.mobilesdkexample.utils.mdocDisplayName
import com.spruceid.mobilesdkexample.utils.credentialTypeDisplayName
import com.spruceid.mobilesdkexample.utils.splitCamelCase
import com.spruceid.mobilesdkexample.viewmodels.StatusListViewModel
import org.json.JSONObject
Expand Down Expand Up @@ -84,6 +84,7 @@ fun GenericCredentialListItemDescriptionFormatter(
val credential = values.toList().firstNotNullOfOrNull {
val cred = credentialPack.getCredentialById(it.first)
val mdoc = cred?.asMsoMdoc()
val dcSdJwt = cred?.asDcSdJwt()
try {
if (
cred?.asJwtVc() != null ||
Expand All @@ -98,6 +99,8 @@ fun GenericCredentialListItemDescriptionFormatter(
it.second.put("issuer", issuer)
}
it.second
} else if (dcSdJwt != null) {
it.second
} else {
null
}
Expand Down Expand Up @@ -126,6 +129,13 @@ fun GenericCredentialListItemDescriptionFormatter(
}
}

if (description.isBlank()) {
try {
description = credential?.getString("issuing_authority").toString()
} catch (_: Exception) {
}
}


Column {
Text(
Expand All @@ -152,7 +162,8 @@ private fun GenericCredentialListItemLeadingIconFormatter(
if (
cred?.asJwtVc() != null ||
cred?.asJsonVc() != null ||
cred?.asSdJwt() != null
cred?.asSdJwt() != null ||
cred?.asDcSdJwt() != null
) {
it.second
} else {
Expand Down Expand Up @@ -231,14 +242,18 @@ fun GenericCredentialListItem(
val cred = credentialPack.getCredentialById(it.first)
try {
val mdoc = cred?.asMsoMdoc()
val dcSdJwt = cred?.asDcSdJwt()
if (
cred?.asJwtVc() != null ||
cred?.asJsonVc() != null ||
cred?.asSdJwt() != null
) {
it.second
} else if (mdoc != null) {
it.second.put("name", mdocDisplayName(mdoc.doctype()))
it.second.put("name", credentialTypeDisplayName(mdoc.doctype()))
it.second
} else if (dcSdJwt != null) {
it.second.put("name", credentialTypeDisplayName(dcSdJwt.vct()))
it.second
} else {
null
Expand Down Expand Up @@ -303,7 +318,7 @@ fun GenericCredentialListItem(
}
}
},
descriptionKeys = listOf("description", "issuer"),
descriptionKeys = listOf("description", "issuer", "issuing_authority"),
descriptionFormatter = { values ->
if (descriptionFormatter != null) {
descriptionFormatter.invoke(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.core.content.ContextCompat
import com.spruceid.mobile.sdk.CredentialPack
import com.spruceid.mobile.sdk.CredentialsViewModel
import com.spruceid.mobile.sdk.rs.Cwt
import com.spruceid.mobile.sdk.rs.IetfSdJwtVc
import com.spruceid.mobile.sdk.rs.JsonVc
import com.spruceid.mobile.sdk.rs.JwtVc
import com.spruceid.mobile.sdk.rs.Mdoc
Expand Down Expand Up @@ -168,6 +169,12 @@ fun addCredential(credentialPack: CredentialPack, rawCredential: String): Creden
} catch (_: Exception) {
}

try {
credentialPack.addDcSdJwt(IetfSdJwtVc.newFromCompactSdJwt(rawCredential))
return credentialPack
} catch (_: Exception) {
}

try {
credentialPack.addJwtVc(JwtVc.newFromCompactJws(rawCredential))
return credentialPack
Expand Down Expand Up @@ -267,6 +274,7 @@ fun getCredentialIdTitleAndIssuer(
claims.entries.firstNotNullOf { claim ->
val c = credentialPack.getCredentialById(claim.key)
val mdoc = c?.asMsoMdoc()
val dcSdJwt = c?.asDcSdJwt()
if (
c?.asSdJwt() != null ||
c?.asJwtVc() != null ||
Expand All @@ -278,26 +286,44 @@ fun getCredentialIdTitleAndIssuer(
if (issuer != null && issuer.toString().isNotBlank()) {
claim.value.put("issuer", issuer)
}
val title = mdocDisplayName(mdoc.doctype())
val title = credentialTypeDisplayName(mdoc.doctype())
claim.value.put("name", title)
claim
} else if (dcSdJwt != null) {
val issuer = claim.value.opt("issuing_authority")
if (issuer != null && issuer.toString().isNotBlank()) {
claim.value.put("issuer", issuer)
}
claim.value.put("name", credentialTypeDisplayName(dcSdJwt.vct()))
claim
} else {
null
}
}
}
// Mdoc
if (credential?.asMsoMdoc() != null || cred.equals(null)) {
cred = claims.entries.firstNotNullOf { claim ->
val mdoc = credentialPack.getCredentialById(claim.key)?.asMsoMdoc()
// dc+sd-jwt: use vct for display name
if (credential?.asDcSdJwt() != null || cred == null) {
cred = claims.entries.firstNotNullOfOrNull { claim ->
val dcSdJwt = credentialPack.getCredentialById(claim.key)?.asDcSdJwt() ?: return@firstNotNullOfOrNull null
val issuer = claim.value.opt("issuing_authority")
if (issuer != null && issuer.toString().isNotBlank()) {
claim.value.put("issuer", issuer)
}
val title = mdoc?.let { mdocDisplayName(it.doctype()) } ?: ""
claim.value.put("name", title)
claim.value.put("name", credentialTypeDisplayName(dcSdJwt.vct()))
claim
}
} ?: cred
}
// Mdoc: use doctype for display name
if (credential?.asMsoMdoc() != null || cred == null) {
cred = claims.entries.firstNotNullOfOrNull { claim ->
val mdoc = credentialPack.getCredentialById(claim.key)?.asMsoMdoc() ?: return@firstNotNullOfOrNull null
val issuer = claim.value.opt("issuing_authority")
if (issuer != null && issuer.toString().isNotBlank()) {
claim.value.put("issuer", issuer)
}
claim.value.put("name", credentialTypeDisplayName(mdoc.doctype()))
claim
} ?: cred
}

val credentialKey = cred.key
Expand Down Expand Up @@ -361,10 +387,11 @@ fun credentialPackHasMdoc(credentialPack: CredentialPack): Boolean {
return false
}

// MARK: - Mdoc Display Name Mapping
// MARK: - Credential Type Display Name Mapping

/** Known mdoc doctype to display name mappings. */
private val mdocDoctypeDisplayNames = mapOf(
/** Known credential type identifier to display name mappings.
* Works for both mdoc doctypes and dc+sd-jwt vct values (shared namespace). */
private val knownCredentialTypeDisplayNames = mapOf(
"org.iso.18013.5.1.mDL" to "Mobile Driver's License",
"org.iso.23220.photoID.1" to "Photo ID",
"org.iso.7367.1.mVRC" to "Mobile Vehicle Registration Certificate",
Expand All @@ -377,18 +404,19 @@ private val mdocDoctypeDisplayNames = mapOf(
)

/**
* Returns a human-readable display name for the given mdoc doctype.
* Falls back to generating a readable name from the doctype string if unknown.
* Returns a human-readable display name for the given credential type identifier.
* Works for both mdoc doctypes and dc+sd-jwt vct values.
* Falls back to generating a readable name from the identifier if unknown.
*/
fun mdocDisplayName(doctype: String): String {
return mdocDoctypeDisplayNames[doctype] ?: humanizeDoctype(doctype)
fun credentialTypeDisplayName(typeIdentifier: String): String {
return knownCredentialTypeDisplayNames[typeIdentifier] ?: humanizeTypeIdentifier(typeIdentifier)
}

/**
* Generates a human-readable name from an unknown doctype.
* Generates a human-readable name from an unknown type identifier.
* Example: "eu.europa.ec.eudi.hiid.1" -> "Hiid"
*/
private fun humanizeDoctype(doctype: String): String {
private fun humanizeTypeIdentifier(doctype: String): String {
val components = doctype.split(".")
if (components.size < 2) return doctype

Expand All @@ -403,15 +431,6 @@ private fun humanizeDoctype(doctype: String): String {
.replace("_", " ")
.split(" ")
.joinToString(" ") { word ->
word.replaceFirstChar { it.uppercaseChar() }.drop(1).lowercase() +
word.firstOrNull()?.uppercaseChar()?.toString().orEmpty()
}.let {
// Fix: properly capitalize first letter of each word
meaningfulComponent
.replace("_", " ")
.split(" ")
.joinToString(" ") { word ->
word.lowercase().replaceFirstChar { it.uppercaseChar() }
}
word.lowercase().replaceFirstChar { it.uppercaseChar() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import com.spruceid.mobilesdkexample.ui.theme.Inter
import com.spruceid.mobilesdkexample.utils.ErrorToast
import com.spruceid.mobilesdkexample.utils.SimpleAlertDialog
import com.spruceid.mobilesdkexample.utils.WarningToast
import com.spruceid.mobilesdkexample.utils.mdocDisplayName
import com.spruceid.mobilesdkexample.utils.credentialTypeDisplayName

@Composable
fun VerifierMDocResultView(
Expand All @@ -48,7 +48,7 @@ fun VerifierMDocResultView(
logVerification: (String, String, String) -> Unit,
) {
val mdoc by remember { mutableStateOf(convertToJson(result)) }
val title = mdocDisplayName(docTypes.firstOrNull() ?: "")
val title = credentialTypeDisplayName(docTypes.firstOrNull() ?: "")
var issuer by remember { mutableStateOf<String?>(null) }

LaunchedEffect(Unit) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ import com.spruceid.mobilesdkexample.ui.theme.Inter
import com.spruceid.mobilesdkexample.utils.activityHiltViewModel
import com.spruceid.mobilesdkexample.utils.getCredentialIdTitleAndIssuer
import com.spruceid.mobilesdkexample.utils.getCurrentSqlDate
import com.spruceid.mobilesdkexample.utils.mdocDisplayName
import com.spruceid.mobilesdkexample.utils.credentialTypeDisplayName
import com.spruceid.mobilesdkexample.viewmodels.CredentialPacksViewModel
import com.spruceid.mobilesdkexample.viewmodels.WalletActivityLogsViewModel
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -560,7 +560,7 @@ fun MdocSelectorItem(
)
)
Text(
text = mdocDisplayName(doctype ?: ""),
text = credentialTypeDisplayName(doctype ?: ""),
fontFamily = Inter,
fontWeight = FontWeight.SemiBold,
fontSize = 18.sp,
Expand Down
Loading