Skip to content

Commit

Permalink
Add accordion component to display generic credentials (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
w4ll3 authored Oct 29, 2024
1 parent 8647810 commit 42df7f5
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 159 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,196 +5,105 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.spruceid.mobilesdkexample.ui.theme.ColorStone500
import com.spruceid.mobilesdkexample.ui.theme.ColorStone950
import com.spruceid.mobilesdkexample.ui.theme.Inter
import com.spruceid.mobilesdkexample.utils.Accordion
import com.spruceid.mobilesdkexample.utils.isDate
import com.spruceid.mobilesdkexample.utils.isImage
import com.spruceid.mobilesdkexample.utils.removeUnderscores
import com.spruceid.mobilesdkexample.utils.splitCamelCase
import org.json.JSONArray
import org.json.JSONObject


@Composable
fun genericObjectDisplayer(obj: JSONObject, filter: List<String>, level: Int = 1): List<Unit> {

fun tryGetJSONObject(key: String): JSONObject? {
try {
obj.getJSONObject(key).let {
return it
}
} catch (_: Exception) {
return null
}
}

fun tryGetJSONArray(key: String): JSONArray? {
try {
obj.getJSONArray(key).let {
return it
}
} catch (_: Exception) {
return null
}
}

fun tryGetJSONObjectFromJSONArray(idx: Int, jsonArray: JSONArray): JSONObject? {
try {
jsonArray.getJSONObject(idx).let {
return it
}
} catch (_: Exception) {
return null
}
}

val res = mutableListOf<Unit>()

obj
.keys()
.asSequence()
.filter { !filter.contains(it) }
.sorted()
.filter { !filter.contains(it) }
.forEach { key ->
val jsonObject = tryGetJSONObject(key)
if (jsonObject != null) {
res.add(
Column {
Text(
key.splitCamelCase().removeUnderscores(),
fontFamily = Inter,
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
color = ColorStone500,
)
Column(
Modifier
.drawWithCache {
onDrawWithContent {

// draw behind the content the vertical line on the left
drawLine(
color = Color.Black,
start = Offset.Zero,
end = Offset(0f, this.size.height),
strokeWidth = 1f
)

// draw the content
drawContent()
}
}
.padding(start = (level * 4).dp)
) {
genericObjectDisplayer(jsonObject, filter, level + 1)
}
if (obj.optJSONObject(key) != null) {
val jsonObject = obj.getJSONObject(key)
res.add(0,
Accordion(
title = key.splitCamelCase().removeUnderscores(),
startExpanded = level < 3,
modifier = Modifier
.padding(start = 12.dp, top = 12.dp, bottom = 12.dp)
) {
genericObjectDisplayer(jsonObject, filter, level + 1)
}
)
} else {
val jsonArray = tryGetJSONArray(key)
if (jsonArray != null) {
for (i in 0 until jsonArray.length()) {
val jsonObjectElem = tryGetJSONObjectFromJSONArray(i, jsonArray)
if (jsonObjectElem != null) {
res.add(
Column {
Text(
"${key.splitCamelCase().removeUnderscores()}.$i",
fontFamily = Inter,
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
color = ColorStone500,
)
Column(
Modifier
.drawWithCache {
onDrawWithContent {

// draw behind the content the vertical line on the left
drawLine(
color = Color.Black,
start = Offset.Zero,
end = Offset(0f, this.size.height),
strokeWidth = 1f
)

// draw the content
drawContent()
}
}
.padding(start = (level * 4).dp)
) {
genericObjectDisplayer(jsonObjectElem, filter, level + 1)
}
}
)
}
}
} else {
val value = obj.get(key).toString()
if (key.lowercase().contains("image") ||
key.lowercase().contains("portrait") ||
value.contains("data:image")
) {
res.add(
Column(Modifier.padding(vertical = 10.dp)) {
Text(
key.splitCamelCase().removeUnderscores(),
fontFamily = Inter,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
color = ColorStone500,
)
CredentialImage(value, key)
}
)
} else if (key.lowercase().contains("date") ||
key.lowercase().contains("from") ||
key.lowercase().contains("until")
) {

res.add(
Column(Modifier.padding(vertical = 10.dp)) {
Text(
key.splitCamelCase().removeUnderscores(),
fontFamily = Inter,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
color = ColorStone500,
)
CredentialDate(value)
}
} else if (obj.optJSONArray(key) != null) {
val jsonArray = obj.getJSONArray(key)
for (i in 0 until jsonArray.length()) {
if (jsonArray.optJSONObject(i) != null) {
val arrayJsonObject = jsonArray.getJSONObject(i)
genericObjectDisplayer(
arrayJsonObject,
filter,
level + 1
)
} else {
res.add(
Column(Modifier.padding(vertical = 10.dp)) {
Column(
Modifier.padding(bottom = 12.dp)
) {
if (i == 0) {
Text(
key.splitCamelCase().removeUnderscores(),
fontFamily = Inter,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
color = ColorStone500
)
Text(
value,
fontFamily = Inter,
fontWeight = FontWeight.Normal,
fontSize = 17.sp,
color = ColorStone950,
color = ColorStone500,
)
}
)
Text(
jsonArray.get(i).toString(),
fontFamily = Inter,
fontWeight = FontWeight.Normal,
fontSize = 17.sp,
color = ColorStone950,
)
}
}

}
} else {
val value = obj.get(key).toString()
res.add(0,
Column(
Modifier.padding(bottom = 12.dp)
) {
Text(
key.splitCamelCase().removeUnderscores(),
fontFamily = Inter,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
color = ColorStone500,
)
if (key.isImage() || value.isImage()) {
CredentialImage(value, key)
} else if (key.isDate()) {
CredentialDate(value)
} else {
Text(
value,
fontFamily = Inter,
fontWeight = FontWeight.Normal,
fontSize = 17.sp,
color = ColorStone950,
)
}
}
)
}
}


return res.toList()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ class GenericCredentialItem : ICredentialView {
null
}
}
genericObjectDisplayer(
genericObjectDisplayer(
credential!!,
listOf("id", "identifier", "type", "proof", "renderMethod", "@context")
)
Expand All @@ -456,7 +456,10 @@ class GenericCredentialItem : ICredentialView {
)
)

Box(Modifier.fillMaxWidth()) {
Box(Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
) {
BaseCard(
credentialPack = credentialPack,
rendering = detailsRendering.toCardRendering()
Expand Down Expand Up @@ -495,7 +498,7 @@ class GenericCredentialItem : ICredentialView {
Column(
Modifier
.fillMaxSize()
.padding(12.dp)
.padding(24.dp)
) {
Text(
text = "Review Info",
Expand All @@ -507,8 +510,10 @@ class GenericCredentialItem : ICredentialView {
modifier = Modifier
.fillMaxWidth()
)
// Header
credentialListItem()

// Body
credentialDetails()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.spruceid.mobilesdkexample.utils

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.spruceid.mobilesdkexample.R
import com.spruceid.mobilesdkexample.ui.theme.ColorStone500
import com.spruceid.mobilesdkexample.ui.theme.ColorStone600
import com.spruceid.mobilesdkexample.ui.theme.Inter

@Composable
fun Accordion(
title: String,
startExpanded: Boolean,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
) {
var expanded by remember { mutableStateOf(startExpanded) }
val density = LocalDensity.current

AccordionHeader(title = title, isExpanded = expanded) {
expanded = !expanded
}
AnimatedVisibility(visible = expanded,
enter = slideInVertically {
with(density) { -40.dp.roundToPx() }
} + expandVertically(
expandFrom = Alignment.Top
) + fadeIn(
initialAlpha = 0.3f
),
exit = slideOutVertically() + shrinkVertically() + fadeOut()

) {
Column(content = content, modifier = modifier)
}
}

@Composable
private fun AccordionHeader(
title: String,
isExpanded: Boolean = false,
onTapped: () -> Unit = {}
) {
val degrees = if (!isExpanded) 90f else 270f

Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp)
.clickable { onTapped() },
) {
Text(
title,
fontFamily = Inter,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
color = ColorStone500,
)
Icon(
painter = painterResource(id = R.drawable.chevron),
contentDescription = "Collapse menu button",
tint = ColorStone600,
modifier = Modifier
.rotate(degrees)
.height(12.dp)
)
}
}
Loading

0 comments on commit 42df7f5

Please sign in to comment.