Skip to content

Commit e5dca98

Browse files
authored
Merge pull request #307 from niscy-eudiw/feat/nested_sd_jwt_claims_v2
Nested sd jwt claims
2 parents d333a5a + 9d7fa1f commit e5dca98

File tree

40 files changed

+1507
-1411
lines changed

40 files changed

+1507
-1411
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ The EUDIW project provides, through this repository, an Android app. Please refe
3535

3636
The app consumes the SDK called EUDIW Wallet core [Wallet core](https://github.com/eu-digital-identity-wallet/eudi-lib-android-wallet-core) and a list of available libraries to facilitate remote presentation, proximity, and issuing test/demo functionality following the specification of the [ARF](https://github.com/eu-digital-identity-wallet/eudi-doc-architecture-and-reference-framework), including:
3737

38-
- OpenID4VP - draft 22 (remote presentation), presentation exchange v2.0,
38+
- OpenID4VP - draft 23 (remote presentation), presentation exchange v2.0,
3939

4040
- ISO18013-5 (proximity presentation),
4141

build-logic/convention/build.gradle.kts

+5-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* governing permissions and limitations under the Licence.
1515
*/
1616

17+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
1718
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
1819

1920
plugins {
@@ -27,8 +28,10 @@ java {
2728
targetCompatibility = JavaVersion.VERSION_17
2829
}
2930
tasks.withType<KotlinCompile>().configureEach {
30-
kotlinOptions {
31-
jvmTarget = JavaVersion.VERSION_17.toString()
31+
kotlin {
32+
compilerOptions {
33+
jvmTarget.set(JvmTarget.JVM_17)
34+
}
3235
}
3336
}
3437

build-logic/convention/src/main/kotlin/EudiWalletCorePlugin.kt

-2
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,8 @@ class EudiWalletCorePlugin : Plugin<Project> {
2525
with(pluginManager) {
2626
apply("kotlinx-serialization")
2727
}
28-
2928
dependencies {
3029
add("api", libs.findLibrary("eudi.wallet.core").get())
31-
add("implementation", libs.findLibrary("cbor-tree").get())
3230
}
3331
}
3432
}

build-logic/convention/src/main/kotlin/project/convention/logic/AndroidCompose.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ internal fun Project.configureAndroidCompose(
5151
add("implementation", libs.findLibrary("accompanist-permissions").get())
5252

5353
add("implementation", libs.findLibrary("androidx.constraintlayout.compose").get())
54-
add("debugImplementation", libs.findLibrary("androidx.compose.ui.tooling").get())
5554
add("implementation", libs.findLibrary("androidx.compose.ui.tooling.preview").get())
55+
add("debugImplementation", libs.findLibrary("androidx.compose.ui.tooling").get())
5656

5757
add("androidTestImplementation", platform(bom))
5858
add("debugImplementation", libs.findLibrary("androidx.compose.ui.testManifest").get())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package eu.europa.ec.commonfeature.extensions
2+
3+
import eu.europa.ec.commonfeature.ui.request.model.DocumentPayloadDomain
4+
import eu.europa.ec.commonfeature.util.keyIsPortrait
5+
import eu.europa.ec.commonfeature.util.keyIsSignature
6+
import eu.europa.ec.corelogic.model.DomainClaim
7+
import eu.europa.ec.eudi.wallet.document.ElementIdentifier
8+
import eu.europa.ec.uilogic.component.AppIcons
9+
import eu.europa.ec.uilogic.component.ListItemData
10+
import eu.europa.ec.uilogic.component.ListItemLeadingContentData
11+
import eu.europa.ec.uilogic.component.ListItemMainContentData
12+
import eu.europa.ec.uilogic.component.ListItemTrailingContentData
13+
import eu.europa.ec.uilogic.component.wrap.CheckboxData
14+
import eu.europa.ec.uilogic.component.wrap.ExpandableListItem
15+
16+
fun DocumentPayloadDomain.toSelectiveExpandableListItems(): List<ExpandableListItem> {
17+
return this.docClaimsDomain.map { claim ->
18+
claim.toSelectiveExpandableListItems(docId)
19+
}
20+
}
21+
22+
fun DomainClaim.toSelectiveExpandableListItems(docId: String): ExpandableListItem {
23+
return when (this) {
24+
is DomainClaim.Group -> {
25+
ExpandableListItem.NestedListItemData(
26+
header = ListItemData(
27+
itemId = path.toId(docId),
28+
mainContentData = ListItemMainContentData.Text(text = displayTitle),
29+
trailingContentData = ListItemTrailingContentData.Icon(iconData = AppIcons.KeyboardArrowDown)
30+
),
31+
nestedItems = items.map {
32+
it.toSelectiveExpandableListItems(docId)
33+
},
34+
isExpanded = false
35+
)
36+
}
37+
38+
is DomainClaim.Primitive -> {
39+
ExpandableListItem.SingleListItemData(
40+
header = ListItemData(
41+
itemId = path.toId(docId),
42+
mainContentData = calculateMainContent(key, value),
43+
overlineText = calculateOverlineText(displayTitle),
44+
leadingContentData = calculateLeadingContent(key, value),
45+
trailingContentData = ListItemTrailingContentData.Checkbox(
46+
checkboxData = CheckboxData(
47+
isChecked = true,
48+
enabled = !isRequired
49+
)
50+
)
51+
)
52+
)
53+
}
54+
}
55+
}
56+
57+
fun DomainClaim.toExpandableListItems(docId: String): ExpandableListItem {
58+
return when (this) {
59+
is DomainClaim.Group -> {
60+
ExpandableListItem.NestedListItemData(
61+
header = ListItemData(
62+
itemId = path.toId(docId),
63+
mainContentData = ListItemMainContentData.Text(text = displayTitle),
64+
trailingContentData = ListItemTrailingContentData.Icon(iconData = AppIcons.KeyboardArrowDown)
65+
),
66+
nestedItems = items.map { it.toExpandableListItems(docId = docId) },
67+
isExpanded = false
68+
)
69+
}
70+
71+
is DomainClaim.Primitive -> {
72+
ExpandableListItem.SingleListItemData(
73+
header = ListItemData(
74+
itemId = path.toId(docId),
75+
mainContentData = calculateMainContent(key, value),
76+
overlineText = calculateOverlineText(displayTitle),
77+
leadingContentData = calculateLeadingContent(key, value),
78+
)
79+
)
80+
}
81+
}
82+
}
83+
84+
private fun calculateMainContent(
85+
key: ElementIdentifier,
86+
value: String,
87+
): ListItemMainContentData {
88+
return when {
89+
keyIsPortrait(key = key) -> {
90+
ListItemMainContentData.Text(text = "")
91+
}
92+
93+
keyIsSignature(key = key) -> {
94+
ListItemMainContentData.Image(base64Image = value)
95+
}
96+
97+
else -> {
98+
ListItemMainContentData.Text(text = value)
99+
}
100+
}
101+
}
102+
103+
private fun calculateLeadingContent(
104+
key: ElementIdentifier,
105+
value: String,
106+
): ListItemLeadingContentData? {
107+
return if (keyIsPortrait(key = key)) {
108+
ListItemLeadingContentData.UserImage(userBase64Image = value)
109+
} else {
110+
null
111+
}
112+
}
113+
114+
private fun calculateOverlineText(displayTitle: String): String? {
115+
return displayTitle.ifBlank {
116+
null
117+
}
118+
}

common-feature/src/main/java/eu/europa/ec/commonfeature/model/DocumentDetailsUi.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package eu.europa.ec.commonfeature.model
1818

1919
import eu.europa.ec.corelogic.model.DocumentIdentifier
2020
import eu.europa.ec.eudi.wallet.document.DocumentId
21-
import eu.europa.ec.uilogic.component.ListItemData
21+
import eu.europa.ec.uilogic.component.wrap.ExpandableListItem
2222

2323
enum class DocumentUiIssuanceState {
2424
Issued, Pending, Failed, Expired
@@ -31,5 +31,5 @@ data class DocumentDetailsUi(
3131
val documentIssuanceState: DocumentUiIssuanceState,
3232
val documentExpirationDateFormatted: String,
3333
val documentHasExpired: Boolean,
34-
val documentDetails: List<ListItemData>,
34+
val documentClaims: List<ExpandableListItem>,
3535
)

common-feature/src/main/java/eu/europa/ec/commonfeature/ui/document_details/domain/DocumentDetailsDomain.kt

+2-9
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,14 @@
1717
package eu.europa.ec.commonfeature.ui.document_details.domain
1818

1919
import eu.europa.ec.corelogic.model.DocumentIdentifier
20+
import eu.europa.ec.corelogic.model.DomainClaim
2021
import eu.europa.ec.eudi.wallet.document.DocumentId
21-
import eu.europa.ec.eudi.wallet.document.ElementIdentifier
22-
23-
data class DocumentItem(
24-
val elementIdentifier: ElementIdentifier,
25-
val value: String,
26-
val readableName: String,
27-
val docId: DocumentId
28-
)
2922

3023
data class DocumentDetailsDomain(
3124
val docName: String,
3225
val docId: DocumentId,
3326
val documentIdentifier: DocumentIdentifier,
3427
val documentExpirationDateFormatted: String,
3528
val documentHasExpired: Boolean,
36-
val detailsItems: List<DocumentItem>
29+
val documentClaims: List<DomainClaim>
3730
)

common-feature/src/main/java/eu/europa/ec/commonfeature/ui/document_details/transformer/DocumentDetailsTransformer.kt

+18-96
Original file line numberDiff line numberDiff line change
@@ -17,46 +17,33 @@
1717
package eu.europa.ec.commonfeature.ui.document_details.transformer
1818

1919
import eu.europa.ec.businesslogic.util.formatInstant
20+
import eu.europa.ec.commonfeature.extensions.toExpandableListItems
2021
import eu.europa.ec.commonfeature.model.DocumentDetailsUi
2122
import eu.europa.ec.commonfeature.model.DocumentUiIssuanceState
2223
import eu.europa.ec.commonfeature.ui.document_details.domain.DocumentDetailsDomain
23-
import eu.europa.ec.commonfeature.ui.document_details.domain.DocumentItem
2424
import eu.europa.ec.commonfeature.util.documentHasExpired
25-
import eu.europa.ec.commonfeature.util.generateUniqueFieldId
26-
import eu.europa.ec.commonfeature.util.keyIsPortrait
27-
import eu.europa.ec.commonfeature.util.keyIsSignature
28-
import eu.europa.ec.commonfeature.util.parseKeyValueUi
29-
import eu.europa.ec.corelogic.extension.getLocalizedClaimName
25+
import eu.europa.ec.commonfeature.util.transformPathsToDomainClaims
26+
import eu.europa.ec.corelogic.extension.toClaimPaths
3027
import eu.europa.ec.corelogic.model.toDocumentIdentifier
3128
import eu.europa.ec.eudi.wallet.document.IssuedDocument
3229
import eu.europa.ec.resourceslogic.provider.ResourceProvider
33-
import eu.europa.ec.uilogic.component.ListItemData
34-
import eu.europa.ec.uilogic.component.ListItemLeadingContentData
35-
import eu.europa.ec.uilogic.component.ListItemMainContentData
3630

3731
object DocumentDetailsTransformer {
3832

3933
fun transformToDocumentDetailsDomain(
4034
document: IssuedDocument,
4135
resourceProvider: ResourceProvider
4236
): Result<DocumentDetailsDomain> = runCatching {
43-
val userLocale = resourceProvider.getLocale()
44-
45-
val detailsDocumentItems = document.data.claims
46-
.map { claim ->
47-
val displayKey: String = claim.metadata?.display.getLocalizedClaimName(
48-
userLocale = userLocale,
49-
fallback = claim.identifier
50-
)
51-
52-
transformToDocumentDetailsDocumentItem(
53-
displayKey = displayKey,
54-
key = claim.identifier,
55-
item = claim.value ?: "",
56-
resourceProvider = resourceProvider,
57-
documentId = document.id
58-
)
59-
}
37+
val claimsPaths = document.data.claims.flatMap { claim ->
38+
claim.toClaimPaths()
39+
}
40+
41+
val domainClaims = transformPathsToDomainClaims(
42+
paths = claimsPaths,
43+
claims = document.data.claims,
44+
metadata = document.metadata,
45+
resourceProvider = resourceProvider,
46+
)
6047

6148
val docHasExpired = documentHasExpired(document.validUntil)
6249

@@ -66,87 +53,22 @@ object DocumentDetailsTransformer {
6653
documentIdentifier = document.toDocumentIdentifier(),
6754
documentExpirationDateFormatted = document.validUntil.formatInstant(),
6855
documentHasExpired = docHasExpired,
69-
detailsItems = detailsDocumentItems
56+
documentClaims = domainClaims
7057
)
7158
}
7259

7360
fun DocumentDetailsDomain.transformToDocumentDetailsUi(): DocumentDetailsUi {
74-
val documentDetailsListItemData = this.detailsItems.toListItemData()
61+
val documentDetailsUi = this.documentClaims.map { domainClaim ->
62+
domainClaim.toExpandableListItems(docId = this.docId)
63+
}
7564
return DocumentDetailsUi(
7665
documentId = this.docId,
7766
documentName = this.docName,
7867
documentIdentifier = this.documentIdentifier,
7968
documentExpirationDateFormatted = this.documentExpirationDateFormatted,
8069
documentHasExpired = this.documentHasExpired,
81-
documentDetails = documentDetailsListItemData,
8270
documentIssuanceState = DocumentUiIssuanceState.Issued,
71+
documentClaims = documentDetailsUi,
8372
)
8473
}
85-
86-
fun List<DocumentItem>.toListItemData(): List<ListItemData> {
87-
return this
88-
.sortedBy { it.readableName.lowercase() }
89-
.map {
90-
91-
val mainContent = when {
92-
keyIsPortrait(key = it.elementIdentifier) -> {
93-
ListItemMainContentData.Text(text = "")
94-
}
95-
96-
keyIsSignature(key = it.elementIdentifier) -> {
97-
ListItemMainContentData.Image(base64Image = it.value)
98-
}
99-
100-
else -> {
101-
ListItemMainContentData.Text(text = it.value)
102-
}
103-
}
104-
105-
val itemId = generateUniqueFieldId(
106-
elementIdentifier = it.elementIdentifier,
107-
documentId = it.docId
108-
)
109-
110-
val leadingContent = if (keyIsPortrait(key = it.elementIdentifier)) {
111-
ListItemLeadingContentData.UserImage(userBase64Image = it.value)
112-
} else {
113-
null
114-
}
115-
116-
ListItemData(
117-
itemId = itemId,
118-
mainContentData = mainContent,
119-
overlineText = it.readableName,
120-
leadingContentData = leadingContent
121-
)
122-
}
123-
}
124-
}
125-
126-
fun transformToDocumentDetailsDocumentItem(
127-
key: String,
128-
displayKey: String,
129-
item: Any,
130-
resourceProvider: ResourceProvider,
131-
documentId: String
132-
): DocumentItem {
133-
134-
val values = StringBuilder()
135-
val localizedKey = displayKey.ifEmpty { key }
136-
137-
parseKeyValueUi(
138-
item = item,
139-
groupIdentifier = localizedKey,
140-
groupIdentifierKey = key,
141-
resourceProvider = resourceProvider,
142-
allItems = values
143-
)
144-
val groupedValues = values.toString()
145-
146-
return DocumentItem(
147-
elementIdentifier = key,
148-
value = groupedValues,
149-
readableName = localizedKey,
150-
docId = documentId
151-
)
15274
}

0 commit comments

Comments
 (0)