diff --git a/MobileSdk/build.gradle.kts b/MobileSdk/build.gradle.kts index 6661a63..84a7a29 100644 --- a/MobileSdk/build.gradle.kts +++ b/MobileSdk/build.gradle.kts @@ -16,8 +16,17 @@ publishing { password = System.getenv("GITHUB_TOKEN") } } + mavenLocal() } publications { + create("debug") { + groupId = "com.spruceid.mobile.sdk" + artifactId = "mobilesdk" + version = System.getenv("VERSION") + + afterEvaluate { from(components["release"]) } + } + // Creates a Maven publication called "release". create("release") { groupId = "com.spruceid.mobile.sdk" @@ -116,7 +125,7 @@ android { } dependencies { - api("com.spruceid.mobile.sdk.rs:mobilesdkrs:0.3.0") + api("com.spruceid.mobile.sdk.rs:mobilesdkrs:0.4.0") //noinspection GradleCompatible implementation("com.android.support:appcompat-v7:28.0.0") /* Begin UI dependencies */ diff --git a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/CredentialPack.kt b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/CredentialPack.kt index 1b10765..d605c78 100644 --- a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/CredentialPack.kt +++ b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/CredentialPack.kt @@ -6,14 +6,13 @@ 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.ParsedCredential -import com.spruceid.mobile.sdk.rs.Vcdm2SdJwt import com.spruceid.mobile.sdk.rs.StorageManagerInterface import com.spruceid.mobile.sdk.rs.Uuid +import com.spruceid.mobile.sdk.rs.Vcdm2SdJwt import com.spruceid.mobile.sdk.rs.VdcCollection import com.spruceid.mobile.sdk.rs.VdcCollectionException import org.json.JSONException import org.json.JSONObject -import java.lang.IllegalArgumentException import java.util.UUID /** @@ -40,6 +39,10 @@ class CredentialPack { this.credentials = credentialsArray } + fun id(): UUID { + return this.id + } + /** * Try to add a credential and throws a ParsingException if not possible */ @@ -103,6 +106,34 @@ class CredentialPack { return credentials } + /** + * Get all status from all credentials async + */ + suspend fun getStatusListsAsync(hasConnection: Boolean): Map { + var res = mutableMapOf() + credentials.forEach { credential -> + val credentialId = credential.id() + credential.asJsonVc()?.let { + if (hasConnection) { + try { + val status = it.status() + if (status.isRevoked()) { + res[credentialId] = CredentialStatusList.REVOKED + } else if (status.isSuspended()) { + res[credentialId] = CredentialStatusList.SUSPENDED + } else { + res[credentialId] = CredentialStatusList.VALID + } + } catch (_: Exception) { + } + } else { + res[credentialId] = CredentialStatusList.UNKNOWN + } + } + } + return res + } + /** * Find claims from all credentials in this CredentialPack. */ @@ -187,12 +218,16 @@ class CredentialPack { try { list().forEach { if (vdcCollection.get(it.id()) == null) { - Log.d("sprucekit", "Saving credential '${it.id()}' " + - "to the VdcCollection") + Log.d( + "sprucekit", "Saving credential '${it.id()}' " + + "to the VdcCollection" + ) vdcCollection.add(it.intoGenericForm()) } else { - Log.d("sprucekit", "Skipped saving credential '${it.id()}' " + - "to the VdcCollection as it already exists") + Log.d( + "sprucekit", "Skipped saving credential '${it.id()}' " + + "to the VdcCollection as it already exists" + ) } } } catch (e: VdcCollectionException) { @@ -269,6 +304,7 @@ class CredentialPackContents { private const val ID_KEY = "id" private const val CREDENTIALS_KEY = "credentials" } + val id: UUID val credentials: List @@ -310,14 +346,18 @@ class CredentialPackContents { try { val credential = vdcCollection.get(it) if (credential == null) { - Log.w("sprucekit", "credential '$it' in pack '${id}'" + - " could not be found") + Log.w( + "sprucekit", "credential '$it' in pack '${id}'" + + " could not be found" + ) Log.d("sprucekit", "VdcCollection: ${vdcCollection.allEntries()}") } credential } catch (e: Exception) { - Log.w("sprucekit" ,"credential '$it' could not be loaded from" + - " storage") + Log.w( + "sprucekit", "credential '$it' could not be loaded from" + + " storage" + ) return@mapNotNull null } } @@ -325,8 +365,10 @@ class CredentialPackContents { try { return@mapNotNull ParsedCredential.parseFromCredential(it) } catch (e: CredentialDecodingException) { - Log.w("sprucekit", "failed to parse credential '${it.id}'" + - " as a known variant") + Log.w( + "sprucekit", "failed to parse credential '${it.id}'" + + " as a known variant" + ) return@mapNotNull null } } @@ -375,3 +417,35 @@ class ParsingException(message: String, cause: Throwable?) : Exception(message, class LoadingException(message: String, cause: Throwable) : Exception(message, cause) class SavingException(message: String, cause: Throwable) : Exception(message, cause) class ClearingException(message: String, cause: Throwable) : Exception(message, cause) + +enum class CredentialStatusList { + /** + * Valid credential + */ + VALID, + + /** + * Credential revoked + */ + REVOKED, + + /** + * Credential suspended + */ + SUSPENDED, + + /** + * No connection + */ + UNKNOWN, + + /** + * Invalid credential + */ + INVALID, + + /** + * Credential doesn't have status list + */ + UNDEFINED +} \ No newline at end of file diff --git a/example/build.gradle.kts b/example/build.gradle.kts index 582d9d1..c0a211b 100644 --- a/example/build.gradle.kts +++ b/example/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "com.spruceid.mobilesdkexample" minSdk = 26 targetSdk = 34 - versionCode = 15 - versionName = "1.1.1" + versionCode = 16 + versionName = "1.2.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -65,6 +65,7 @@ dependencies { implementation("androidx.activity:activity-compose:1.8.2") implementation(platform("androidx.compose:compose-bom:2023.03.00")) implementation("com.google.accompanist:accompanist-permissions:0.35.1-alpha") + implementation("com.google.accompanist:accompanist-swiperefresh:0.36.0") implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-tooling-preview") diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/HomeView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/HomeView.kt index f3cda1c..bc14f6d 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/HomeView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/HomeView.kt @@ -40,6 +40,7 @@ import com.spruceid.mobilesdkexample.ui.theme.ColorBlue900 import com.spruceid.mobilesdkexample.ui.theme.Switzer import com.spruceid.mobilesdkexample.verifier.VerifierHomeView import com.spruceid.mobilesdkexample.viewmodels.CredentialPacksViewModel +import com.spruceid.mobilesdkexample.viewmodels.StatusListViewModel import com.spruceid.mobilesdkexample.viewmodels.HelpersViewModel import com.spruceid.mobilesdkexample.viewmodels.VerificationMethodsViewModel import com.spruceid.mobilesdkexample.wallet.WalletHomeView @@ -55,6 +56,7 @@ fun HomeView( initialTab: String, verificationMethodsViewModel: VerificationMethodsViewModel, credentialPacksViewModel: CredentialPacksViewModel, + statusListViewModel: StatusListViewModel, helpersViewModel: HelpersViewModel ) { var tab by remember { @@ -79,6 +81,7 @@ fun HomeView( WalletHomeView( navController, credentialPacksViewModel = credentialPacksViewModel, + statusListViewModel = statusListViewModel, helpersViewModel = helpersViewModel ) } else { diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt b/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt index fe551f6..952b974 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController +import com.spruceid.mobile.sdk.ConnectionLiveData import com.spruceid.mobilesdkexample.db.AppDatabase import com.spruceid.mobilesdkexample.db.VerificationActivityLogsRepository import com.spruceid.mobilesdkexample.db.VerificationMethodsRepository @@ -22,6 +23,7 @@ import com.spruceid.mobilesdkexample.ui.theme.MobileSdkTheme import com.spruceid.mobilesdkexample.viewmodels.CredentialPacksViewModel import com.spruceid.mobilesdkexample.viewmodels.CredentialPacksViewModelFactory import com.spruceid.mobilesdkexample.viewmodels.HelpersViewModel +import com.spruceid.mobilesdkexample.viewmodels.StatusListViewModel import com.spruceid.mobilesdkexample.viewmodels.VerificationActivityLogsViewModel import com.spruceid.mobilesdkexample.viewmodels.VerificationActivityLogsViewModelFactory import com.spruceid.mobilesdkexample.viewmodels.VerificationMethodsViewModel @@ -29,6 +31,7 @@ import com.spruceid.mobilesdkexample.viewmodels.VerificationMethodsViewModelFact class MainActivity : ComponentActivity() { private lateinit var navController: NavHostController + private lateinit var connectionLiveData: ConnectionLiveData override fun onNewIntent(intent: Intent?) { if (intent != null && intent.action == "android.intent.action.VIEW" && intent.data != null) { @@ -83,6 +86,14 @@ class MainActivity : ComponentActivity() { CredentialPacksViewModelFactory(application as MainApplication) } + val statusListViewModel: StatusListViewModel by viewModels() + connectionLiveData = ConnectionLiveData(this) + connectionLiveData.observe(this) { isNetworkAvailable -> + isNetworkAvailable?.let { + statusListViewModel.setHasConnection(it) + } + } + val helpersViewModel: HelpersViewModel by viewModels() SetupNavGraph( @@ -90,6 +101,7 @@ class MainActivity : ComponentActivity() { verificationMethodsViewModel = verificationMethodsViewModel, verificationActivityLogsViewModel = verificationActivityLogsViewModel, credentialPacksViewModel = credentialPacksViewModel, + statusListViewModel = statusListViewModel, helpersViewModel = helpersViewModel ) } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/credentials/AddToWalletView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/credentials/AddToWalletView.kt index 8265ba4..dadcf4f 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/credentials/AddToWalletView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/credentials/AddToWalletView.kt @@ -33,6 +33,7 @@ import com.spruceid.mobilesdkexample.ui.theme.ColorStone950 import com.spruceid.mobilesdkexample.ui.theme.Inter import com.spruceid.mobilesdkexample.utils.credentialDisplaySelector import com.spruceid.mobilesdkexample.viewmodels.CredentialPacksViewModel +import com.spruceid.mobilesdkexample.viewmodels.StatusListViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.launch @@ -41,7 +42,8 @@ import kotlinx.coroutines.launch fun AddToWalletView( navController: NavHostController, rawCredential: String, - credentialPacksViewModel: CredentialPacksViewModel + credentialPacksViewModel: CredentialPacksViewModel, + statusListViewModel: StatusListViewModel ) { var credentialItem by remember { mutableStateOf(null) } var err by remember { mutableStateOf(null) } @@ -50,7 +52,7 @@ fun AddToWalletView( val scope = rememberCoroutineScope() LaunchedEffect(Unit) { - credentialItem = credentialDisplaySelector(rawCredential, null, null) + credentialItem = credentialDisplaySelector(rawCredential, statusListViewModel, null, null) } fun back() { diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/credentials/CredentialStatus.kt b/example/src/main/java/com/spruceid/mobilesdkexample/credentials/CredentialStatus.kt new file mode 100644 index 0000000..f4bb6ae --- /dev/null +++ b/example/src/main/java/com/spruceid/mobilesdkexample/credentials/CredentialStatus.kt @@ -0,0 +1,349 @@ +package com.spruceid.mobilesdkexample.credentials + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.spruceid.mobile.sdk.CredentialStatusList +import com.spruceid.mobilesdkexample.R +import com.spruceid.mobilesdkexample.ui.theme.ColorBase50 +import com.spruceid.mobilesdkexample.ui.theme.ColorEmerald600 +import com.spruceid.mobilesdkexample.ui.theme.ColorRose700 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone100 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone300 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone500 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone950 +import com.spruceid.mobilesdkexample.ui.theme.ColorYellow700 +import com.spruceid.mobilesdkexample.ui.theme.Inter + +@Composable +fun CredentialStatusSmall(status: CredentialStatusList) { + when (status) { + CredentialStatusList.VALID -> Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(id = R.drawable.valid), + contentDescription = stringResource(id = R.string.valid_check), + colorFilter = ColorFilter.tint(ColorEmerald600), + modifier = Modifier + .width(14.dp) + .height(14.dp) + .padding(end = 3.dp) + ) + Text( + text = "Valid", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 11.sp, + color = ColorEmerald600 + ) + } + + CredentialStatusList.REVOKED -> Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(id = R.drawable.invalid), + contentDescription = stringResource(id = R.string.revoked_check), + colorFilter = ColorFilter.tint(ColorRose700), + modifier = Modifier + .width(14.dp) + .height(14.dp) + .padding(end = 3.dp) + ) + Text( + text = "Revoked", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 11.sp, + color = ColorRose700 + ) + } + + CredentialStatusList.SUSPENDED -> Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(id = R.drawable.suspended), + contentDescription = stringResource(id = R.string.suspended_check), + colorFilter = ColorFilter.tint(ColorYellow700), + modifier = Modifier + .width(14.dp) + .height(14.dp) + .padding(end = 3.dp) + ) + Text( + text = "Suspended", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 11.sp, + color = ColorYellow700 + ) + } + + CredentialStatusList.UNKNOWN -> Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(id = R.drawable.unknown), + contentDescription = stringResource(id = R.string.unknown_check), + colorFilter = ColorFilter.tint(ColorStone950), + modifier = Modifier + .width(14.dp) + .height(14.dp) + .padding(end = 3.dp) + ) + Text( + text = "Unknown", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 11.sp, + color = ColorStone950 + ) + } + + CredentialStatusList.INVALID -> Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(id = R.drawable.invalid), + contentDescription = stringResource(id = R.string.invalid_check), + colorFilter = ColorFilter.tint(ColorRose700), + modifier = Modifier + .width(14.dp) + .height(14.dp) + .padding(end = 3.dp) + ) + Text( + text = "Invalid", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 11.sp, + color = ColorRose700 + ) + } + + CredentialStatusList.UNDEFINED -> {} + } +} + +@Composable +fun CredentialStatus(status: CredentialStatusList) { + when (status) { + CredentialStatusList.VALID -> Column { + Text( + "Status", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + color = ColorStone500, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .clip(RoundedCornerShape(6.dp)) + .background(ColorEmerald600) + .border( + width = 1.dp, + color = ColorEmerald600, + shape = RoundedCornerShape(6.dp) + ) + ) { + Image( + painter = painterResource(id = R.drawable.valid), + contentDescription = stringResource(id = R.string.valid_check), + colorFilter = ColorFilter.tint(ColorBase50), + modifier = Modifier + .width(24.dp) + .height(24.dp) + .padding(end = 3.dp) + ) + Text( + text = "VALID", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 18.sp, + color = ColorBase50 + ) + } + } + + CredentialStatusList.REVOKED -> Column { + Text( + "Status", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + color = ColorStone500, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .clip(RoundedCornerShape(6.dp)) + .background(ColorRose700) + .border( + width = 1.dp, + color = ColorRose700, + shape = RoundedCornerShape(6.dp) + ) + ) { + Image( + painter = painterResource(id = R.drawable.invalid), + contentDescription = stringResource(id = R.string.revoked_check), + colorFilter = ColorFilter.tint(ColorBase50), + modifier = Modifier + .width(24.dp) + .height(24.dp) + .padding(end = 3.dp) + ) + Text( + text = "REVOKED", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 18.sp, + color = ColorBase50 + ) + } + } + + CredentialStatusList.SUSPENDED -> Column { + Text( + "Status", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + color = ColorStone500, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .clip(RoundedCornerShape(6.dp)) + .background(ColorYellow700) + .border( + width = 1.dp, + color = ColorYellow700, + shape = RoundedCornerShape(6.dp) + ) + ) { + Image( + painter = painterResource(id = R.drawable.suspended), + contentDescription = stringResource(id = R.string.suspended_check), + colorFilter = ColorFilter.tint(ColorBase50), + modifier = Modifier + .width(24.dp) + .height(24.dp) + .padding(end = 3.dp) + ) + Text( + text = "SUSPENDED", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 18.sp, + color = ColorBase50 + ) + } + } + + CredentialStatusList.UNKNOWN -> Column { + Text( + "Status", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + color = ColorStone500, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .clip(RoundedCornerShape(6.dp)) + .background(ColorStone100) + .border( + width = 1.dp, + color = ColorStone300, + shape = RoundedCornerShape(6.dp) + ) + ) { + Image( + painter = painterResource(id = R.drawable.unknown), + contentDescription = stringResource(id = R.string.unknown_check), + colorFilter = ColorFilter.tint(ColorStone950), + modifier = Modifier + .width(24.dp) + .height(24.dp) + .padding(end = 3.dp) + ) + Text( + text = "UNKNOWN", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 18.sp, + color = ColorStone950 + ) + } + } + + CredentialStatusList.INVALID -> Column { + Text( + "Status", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + color = ColorStone500, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .clip(RoundedCornerShape(6.dp)) + .background(ColorRose700) + .border( + width = 1.dp, + color = ColorRose700, + shape = RoundedCornerShape(6.dp) + ) + ) { + Image( + painter = painterResource(id = R.drawable.invalid), + contentDescription = stringResource(id = R.string.invalid_check), + colorFilter = ColorFilter.tint(ColorBase50), + modifier = Modifier + .width(24.dp) + .height(24.dp) + .padding(end = 3.dp) + ) + Text( + text = "INVALID", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 18.sp, + color = ColorBase50 + ) + } + } + + CredentialStatusList.UNDEFINED -> {} + } +} \ No newline at end of file diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/credentials/GenericCredentialItem.kt b/example/src/main/java/com/spruceid/mobilesdkexample/credentials/GenericCredentialItem.kt index 0c1291b..008ac56 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/credentials/GenericCredentialItem.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/credentials/GenericCredentialItem.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -20,6 +19,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -35,6 +35,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.spruceid.mobile.sdk.CredentialPack +import com.spruceid.mobile.sdk.CredentialStatusList import com.spruceid.mobile.sdk.ui.BaseCard import com.spruceid.mobile.sdk.ui.CardRenderingDetailsField import com.spruceid.mobile.sdk.ui.CardRenderingDetailsView @@ -48,35 +49,43 @@ import com.spruceid.mobilesdkexample.ui.theme.ColorStone950 import com.spruceid.mobilesdkexample.ui.theme.Inter import com.spruceid.mobilesdkexample.utils.addCredential import com.spruceid.mobilesdkexample.utils.splitCamelCase +import com.spruceid.mobilesdkexample.viewmodels.StatusListViewModel +import kotlinx.coroutines.launch import org.json.JSONObject class GenericCredentialItem : ICredentialView { override var credentialPack: CredentialPack + private val statusListViewModel: StatusListViewModel private val onDelete: (() -> Unit)? private val onExport: ((String) -> Unit)? constructor( credentialPack: CredentialPack, + statusListViewModel: StatusListViewModel, onDelete: (() -> Unit)? = null, onExport: ((String) -> Unit)? = null ) { this.credentialPack = credentialPack this.onDelete = onDelete this.onExport = onExport + this.statusListViewModel = statusListViewModel } constructor( rawCredential: String, + statusListViewModel: StatusListViewModel, onDelete: (() -> Unit)? = null, onExport: ((String) -> Unit)? = null ) { this.credentialPack = addCredential(CredentialPack(), rawCredential) this.onDelete = onDelete this.onExport = onExport + this.statusListViewModel = statusListViewModel } @Composable private fun descriptionFormatter(values: Map) { + val statusLists by statusListViewModel.statusLists.collectAsState() val credential = values.toList().firstNotNullOfOrNull { val cred = credentialPack.getCredentialById(it.first) try { @@ -115,7 +124,9 @@ class GenericCredentialItem : ICredentialView { fontSize = 14.sp, color = ColorStone600 ) - Spacer(modifier = Modifier.height(16.dp)) + CredentialStatusSmall( + statusLists[credentialPack.id()] ?: CredentialStatusList.UNDEFINED + ) } } @@ -369,6 +380,7 @@ class GenericCredentialItem : ICredentialView { @Composable override fun credentialDetails() { + val statusLists by statusListViewModel.statusLists.collectAsState() val detailsRendering = CardRenderingDetailsView( fields = listOf( CardRenderingDetailsField( @@ -391,10 +403,22 @@ class GenericCredentialItem : ICredentialView { null } } - genericObjectDisplayer( - credential!!, - listOf("type", "hashed", "salt", "proof", "renderMethod", "@context") - ) + Column { + CredentialStatus( + statusLists[credentialPack.id()] ?: CredentialStatusList.UNDEFINED + ) + genericObjectDisplayer( + credential!!, + listOf( + "type", + "hashed", + "salt", + "proof", + "renderMethod", + "@context" + ) + ) + } } ) ) @@ -415,9 +439,7 @@ class GenericCredentialItem : ICredentialView { @OptIn(ExperimentalMaterial3Api::class) @Composable override fun credentialPreviewAndDetails() { - var sheetOpen by remember { - mutableStateOf(false) - } + var sheetOpen by remember { mutableStateOf(false) } Box( Modifier diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt b/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt index 7522392..e286ce1 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt @@ -19,6 +19,7 @@ import com.spruceid.mobilesdkexample.verifiersettings.VerifierSettingsActivityLo import com.spruceid.mobilesdkexample.verifiersettings.VerifierSettingsHomeView import com.spruceid.mobilesdkexample.viewmodels.CredentialPacksViewModel import com.spruceid.mobilesdkexample.viewmodels.HelpersViewModel +import com.spruceid.mobilesdkexample.viewmodels.StatusListViewModel import com.spruceid.mobilesdkexample.viewmodels.VerificationActivityLogsViewModel import com.spruceid.mobilesdkexample.viewmodels.VerificationMethodsViewModel import com.spruceid.mobilesdkexample.wallet.DispatchQRView @@ -32,6 +33,7 @@ fun SetupNavGraph( verificationMethodsViewModel: VerificationMethodsViewModel, verificationActivityLogsViewModel: VerificationActivityLogsViewModel, credentialPacksViewModel: CredentialPacksViewModel, + statusListViewModel: StatusListViewModel, helpersViewModel: HelpersViewModel ) { NavHost(navController = navController, startDestination = Screen.HomeScreen.route) { @@ -49,6 +51,7 @@ fun SetupNavGraph( initialTab = tab, verificationMethodsViewModel = verificationMethodsViewModel, credentialPacksViewModel = credentialPacksViewModel, + statusListViewModel = statusListViewModel, helpersViewModel = helpersViewModel ) } @@ -89,7 +92,8 @@ fun SetupNavGraph( navController, verificationId = id, verificationMethodsViewModel = verificationMethodsViewModel, - verificationActivityLogsViewModel = verificationActivityLogsViewModel + verificationActivityLogsViewModel = verificationActivityLogsViewModel, + statusListViewModel = statusListViewModel ) } composable( @@ -126,7 +130,12 @@ fun SetupNavGraph( listOf(navDeepLink { uriPattern = "spruceid://?sd-jwt={rawCredential}" }) ) { backStackEntry -> val rawCredential = backStackEntry.arguments?.getString("rawCredential")!! - AddToWalletView(navController, rawCredential, credentialPacksViewModel) + AddToWalletView( + navController, + rawCredential, + credentialPacksViewModel, + statusListViewModel + ) } composable( route = Screen.ScanQRScreen.route, @@ -135,7 +144,7 @@ fun SetupNavGraph( route = Screen.HandleOID4VCI.route, ) { backStackEntry -> val url = backStackEntry.arguments?.getString("url")!! - HandleOID4VCIView(navController, url, credentialPacksViewModel) + HandleOID4VCIView(navController, url, credentialPacksViewModel, statusListViewModel) } composable( route = Screen.HandleOID4VP.route, diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt b/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt index a723c34..ce6b762 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt @@ -18,6 +18,7 @@ import com.spruceid.mobile.sdk.rs.Uuid import com.spruceid.mobile.sdk.rs.Vcdm2SdJwt import com.spruceid.mobilesdkexample.credentials.GenericCredentialItem import com.spruceid.mobilesdkexample.credentials.ICredentialView +import com.spruceid.mobilesdkexample.viewmodels.StatusListViewModel import org.json.JSONObject import java.sql.Date import java.text.SimpleDateFormat @@ -123,6 +124,7 @@ fun keyPathFinder(json: Any, path: MutableList): Any { fun credentialDisplaySelector( rawCredential: String, + statusListViewModel: StatusListViewModel, onDelete: (() -> Unit)?, onExport: ((String) -> Unit)? ): ICredentialView { @@ -133,7 +135,7 @@ fun credentialDisplaySelector( // credentialPack.addSdJwt(Vcdm2SdJwt.newFromCompactSdJwt(rawCredential)) // return AchievementCredentialItem(credentialPack, onDelete) // } catch (_: Exception) { - return GenericCredentialItem(rawCredential, onDelete, onExport) + return GenericCredentialItem(rawCredential, statusListViewModel, onDelete, onExport) // } } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifierCredentialSuccessView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifierCredentialSuccessView.kt index e14e2f2..ab42b5c 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifierCredentialSuccessView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifierCredentialSuccessView.kt @@ -28,14 +28,16 @@ import com.spruceid.mobilesdkexample.ui.theme.ColorStone950 import com.spruceid.mobilesdkexample.ui.theme.Inter import com.spruceid.mobilesdkexample.utils.credentialDisplaySelector import com.spruceid.mobilesdkexample.utils.splitCamelCase +import com.spruceid.mobilesdkexample.viewmodels.StatusListViewModel @Composable fun VerifierCredentialSuccessView( rawCredential: String, onClose: () -> Unit, - logVerification: (String, String) -> Unit + logVerification: (String, String) -> Unit, + statusListViewModel: StatusListViewModel ) { - val credentialItem = credentialDisplaySelector(rawCredential, null, null) + val credentialItem = credentialDisplaySelector(rawCredential, statusListViewModel = statusListViewModel, null, null) var title by remember { mutableStateOf(null) } var issuer by remember { mutableStateOf(null) } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyDelegatedOid4vpView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyDelegatedOid4vpView.kt index b5881d2..b6640a8 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyDelegatedOid4vpView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifyDelegatedOid4vpView.kt @@ -37,6 +37,7 @@ import com.spruceid.mobilesdkexample.ui.theme.ColorStone300 import com.spruceid.mobilesdkexample.ui.theme.ColorStone950 import com.spruceid.mobilesdkexample.ui.theme.Inter import com.spruceid.mobilesdkexample.utils.getCurrentSqlDate +import com.spruceid.mobilesdkexample.viewmodels.StatusListViewModel import com.spruceid.mobilesdkexample.viewmodels.VerificationActivityLogsViewModel import com.spruceid.mobilesdkexample.viewmodels.VerificationMethodsViewModel import io.ktor.http.Url @@ -55,6 +56,7 @@ fun VerifyDelegatedOid4vpView( verificationId: String, verificationMethodsViewModel: VerificationMethodsViewModel, verificationActivityLogsViewModel: VerificationActivityLogsViewModel, + statusListViewModel: StatusListViewModel ) { val scope = rememberCoroutineScope() @@ -77,7 +79,9 @@ fun VerifyDelegatedOid4vpView( try { scope.launch { val res = - verifier.pollVerificationStatus("$uri?status=${status.toString().lowercase()}") + verifier.pollVerificationStatus( + "$uri?status=${status.toString().lowercase()}" + ) when (res.status) { DelegatedVerifierStatus.INITIATED -> { monitorStatus(res.status) @@ -196,7 +200,8 @@ fun VerifyDelegatedOid4vpView( ) ) } - } + }, + statusListViewModel = statusListViewModel ) } } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/StatusListViewModel.kt b/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/StatusListViewModel.kt new file mode 100644 index 0000000..9685c7c --- /dev/null +++ b/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/StatusListViewModel.kt @@ -0,0 +1,43 @@ +package com.spruceid.mobilesdkexample.viewmodels + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import com.spruceid.mobile.sdk.CredentialPack +import com.spruceid.mobile.sdk.CredentialStatusList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import java.util.UUID + +class StatusListViewModel(application: Application) : AndroidViewModel(application) { + private val _statusLists = MutableStateFlow(mutableMapOf()) + val statusLists = _statusLists.asStateFlow() + private val _hasConnection = MutableStateFlow(false) + val hasConnection = _hasConnection.asStateFlow() + + suspend fun fetchStatus(credentialPack: CredentialPack): CredentialStatusList { + val statusLists = credentialPack.getStatusListsAsync(hasConnection.value) + + if (statusLists.isEmpty()) { + return CredentialStatusList.UNDEFINED + } else { + return statusLists.entries.first().value + } + } + + fun getStatusLists(credentialPacks: List) { + CoroutineScope(Dispatchers.IO).launch { + val tmpMap = mutableMapOf() + credentialPacks.forEach { credentialPack -> + tmpMap[credentialPack.id()] = fetchStatus(credentialPack) + } + _statusLists.value = tmpMap + } + } + + fun setHasConnection(connected: Boolean) { + _hasConnection.value = connected + } +} \ No newline at end of file diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VCIView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VCIView.kt index e2ea96d..6a63ca8 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VCIView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VCIView.kt @@ -16,6 +16,7 @@ import com.spruceid.mobile.sdk.rs.DidMethod import com.spruceid.mobile.sdk.rs.HttpRequest import com.spruceid.mobile.sdk.rs.HttpResponse import com.spruceid.mobile.sdk.rs.Oid4vci +import com.spruceid.mobile.sdk.rs.Oid4vciExchangeOptions import com.spruceid.mobile.sdk.rs.generatePopComplete import com.spruceid.mobile.sdk.rs.generatePopPrepare import com.spruceid.mobilesdkexample.ErrorView @@ -24,6 +25,7 @@ import com.spruceid.mobilesdkexample.R import com.spruceid.mobilesdkexample.credentials.AddToWalletView import com.spruceid.mobilesdkexample.navigation.Screen import com.spruceid.mobilesdkexample.viewmodels.CredentialPacksViewModel +import com.spruceid.mobilesdkexample.viewmodels.StatusListViewModel import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO import io.ktor.client.request.request @@ -36,7 +38,8 @@ import io.ktor.util.toMap fun HandleOID4VCIView( navController: NavHostController, url: String, - credentialPacksViewModel: CredentialPacksViewModel + credentialPacksViewModel: CredentialPacksViewModel, + statusListViewModel: StatusListViewModel ) { var loading by remember { mutableStateOf(false) } var err by remember { mutableStateOf(null) } @@ -128,7 +131,10 @@ fun HandleOID4VCIView( oid4vciSession.setContextMap(getVCPlaygroundOID4VCIContext(ctx = ctx)) val credentials = - pop?.let { oid4vciSession.exchangeCredential(proofsOfPossession = listOf(pop)) } + pop?.let { oid4vciSession.exchangeCredential( + proofsOfPossession = listOf(pop), + options = Oid4vciExchangeOptions(true), + ) } credentials?.forEach { cred -> cred.payload.toString(Charsets.UTF_8).let { credential = it } @@ -152,7 +158,8 @@ fun HandleOID4VCIView( AddToWalletView( navController = navController, rawCredential = credential!!, - credentialPacksViewModel = credentialPacksViewModel + credentialPacksViewModel = credentialPacksViewModel, + statusListViewModel = statusListViewModel ) } } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VPView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VPView.kt index df16169..31f4e46 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VPView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VPView.kt @@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll @@ -30,6 +32,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.ParagraphStyle @@ -42,10 +45,14 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController +import com.spruceid.mobile.sdk.KeyManager +import com.spruceid.mobile.sdk.rs.DidMethod +import com.spruceid.mobile.sdk.rs.DidMethodUtils import com.spruceid.mobile.sdk.rs.Holder import com.spruceid.mobile.sdk.rs.ParsedCredential import com.spruceid.mobile.sdk.rs.PermissionRequest import com.spruceid.mobile.sdk.rs.PermissionResponse +import com.spruceid.mobile.sdk.rs.PresentationSigner import com.spruceid.mobile.sdk.rs.RequestedField import com.spruceid.mobilesdkexample.ErrorView import com.spruceid.mobilesdkexample.LoadingView @@ -66,11 +73,50 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.json.JSONObject +class Signer(keyId: String?) : PresentationSigner { + private val keyId = if (keyId == null) "reference-app/default-signing" else keyId + private val keyManager = KeyManager() + private val jwk = keyManager.getJwk(this.keyId) ?: throw IllegalArgumentException("Invalid kid") + private val didJwk = DidMethodUtils(DidMethod.JWK) + + override suspend fun sign(payload: ByteArray): ByteArray { + val signature = + keyManager.signPayload(keyId, payload) + ?: throw IllegalStateException("Failed to sign payload") + + return signature + } + + override fun algorithm(): String { + // Parse the jwk as a JSON object and return the "alg" field + var json = JSONObject(jwk) + + return json.getString("alg") + } + + override suspend fun verificationMethod(): String { + return didJwk.vmFromJwk(jwk) + } + + override fun did(): String { + return didJwk.didFromJwk(jwk) + } + + override fun jwk(): String { + return jwk + } + + override fun cryptosuite(): String { + // TODO: Add an uniffi enum type for crypto suites. + return "ecdsa-rdfc-2019" + } +} + @Composable fun HandleOID4VPView( - navController: NavController, - url: String, - credentialPacksViewModel: CredentialPacksViewModel + navController: NavController, + url: String, + credentialPacksViewModel: CredentialPacksViewModel ) { val scope = rememberCoroutineScope() val credentialPacks = credentialPacksViewModel.credentialPacks @@ -80,23 +126,29 @@ fun HandleOID4VPView( var permissionRequest by remember { mutableStateOf(null) } var permissionResponse by remember { mutableStateOf(null) } var selectedCredential by remember { mutableStateOf(null) } + val ctx = LocalContext.current - var err by remember { - mutableStateOf(null) - } + var err by remember { mutableStateOf(null) } LaunchedEffect(Unit) { try { val credentials = mutableListOf() - credentialPacks.value - .forEach { credentialPack -> - credentials.addAll(credentialPack.list()) - credentialClaims += credentialPack.findCredentialClaims(listOf("name", "type")) - } + credentialPacks.value.forEach { credentialPack -> + credentials.addAll(credentialPack.list()) + credentialClaims += credentialPack.findCredentialClaims(listOf("name", "type")) + } withContext(Dispatchers.IO) { - holder = Holder.newWithCredentials(credentials, trustedDids); - permissionRequest = holder!!.authorizationRequest(url) + val signer = Signer("reference-app/default-signing") + holder = + Holder.newWithCredentials( + credentials, + trustedDids, + signer, + getVCPlaygroundOID4VCIContext(ctx) + ) + val newurl = url.replace("authorize", "") + permissionRequest = holder!!.authorizationRequest(newurl) } } catch (e: Exception) { err = e.localizedMessage @@ -104,20 +156,14 @@ fun HandleOID4VPView( } fun onBack() { - navController.navigate(Screen.HomeScreen.route) { - popUpTo(0) - } + navController.navigate(Screen.HomeScreen.route) { popUpTo(0) } } if (err != null) { ErrorView( - errorTitle = "Error Presenting Credential", - errorDetails = err!!, - onClose = { - navController.navigate(Screen.HomeScreen.route) { - popUpTo(0) - } - } + errorTitle = "Error Presenting Credential", + errorDetails = err!!, + onClose = { navController.navigate(Screen.HomeScreen.route) { popUpTo(0) } } ) } else { if (permissionRequest == null) { @@ -125,167 +171,149 @@ fun HandleOID4VPView( } else if (permissionResponse == null) { if (permissionRequest!!.credentials().isNotEmpty()) { CredentialSelector( - credentials = permissionRequest!!.credentials(), - credentialClaims = credentialClaims, - getRequestedFields = { credential -> - permissionRequest!!.requestedFields( - credential - ) - }, - onContinue = { selectedCredentials -> - scope.launch { - try { - // TODO: support multiple presentation - selectedCredential = selectedCredentials.first() - permissionResponse = permissionRequest!!.createPermissionResponse( - listOf(selectedCredential!!) - ) - } catch (e: Exception) { - err = e.localizedMessage + credentials = permissionRequest!!.credentials(), + credentialClaims = credentialClaims, + getRequestedFields = { credential -> + permissionRequest!!.requestedFields(credential) + }, + onContinue = { selectedCredentials -> + scope.launch { + try { + permissionResponse = + permissionRequest!!.createPermissionResponse( + selectedCredentials + ) + } catch (e: Exception) { + err = e.localizedMessage + } } - } - }, - onCancel = { - onBack() - } + }, + onCancel = { onBack() } ) } else { ErrorView( - errorTitle = "No matching credential(s)", - errorDetails = "There are no credentials in your wallet that match the verification request you have scanned", - closeButtonLabel = "Cancel" + errorTitle = "No matching credential(s)", + errorDetails = + "There are no credentials in your wallet that match the verification request you have scanned", + closeButtonLabel = "Cancel" ) { onBack() } } } else { - DataFieldSelector( - requestedFields = permissionRequest!!.requestedFields(selectedCredential!!), - onContinue = { - scope.launch { - try { - holder!!.submitPermissionResponse(permissionResponse!!) - onBack() - } catch (e: Exception) { - err = e.localizedMessage - } - } - }, - onCancel = { - onBack() + LazyColumn { + items(permissionResponse!!.selectedCredentials()) { selectedCredential -> + DataFieldSelector( + requestedFields = + permissionRequest!!.requestedFields(selectedCredential), + onContinue = { + scope.launch { + try { + holder!!.submitPermissionResponse(permissionResponse!!) + onBack() + } catch (e: Exception) { + err = e.localizedMessage + } + } + }, + onCancel = { onBack() } + ) } - ) + } } } } @Composable fun DataFieldSelector( - requestedFields: List, - onContinue: () -> Unit, - onCancel: () -> Unit + requestedFields: List, + onContinue: () -> Unit, + onCancel: () -> Unit ) { val bullet = "\u2022" val paragraphStyle = ParagraphStyle(textIndent = TextIndent(restLine = 12.sp)) - val mockDataField = requestedFields.map { field -> - field.name()?.replaceFirstChar(Char::titlecase) ?: "" - } + val mockDataField = + requestedFields.map { field -> field.name()?.replaceFirstChar(Char::titlecase) ?: "" } - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp) - .padding(top = 48.dp) - ) { + Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp).padding(top = 48.dp)) { Text( - buildAnnotatedString { - withStyle(style = SpanStyle(color = Color.Blue)) { - append("Verifier") - } - append(" is requesting access to the following information") - }, - fontFamily = Inter, - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - color = ColorStone950, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp), - textAlign = TextAlign.Center + buildAnnotatedString { + withStyle(style = SpanStyle(color = Color.Blue)) { append("Verifier") } + append(" is requesting access to the following information") + }, + fontFamily = Inter, + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + color = ColorStone950, + modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp), + textAlign = TextAlign.Center ) Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .weight(weight = 1f, fill = false) + modifier = + Modifier.fillMaxSize() + .verticalScroll(rememberScrollState()) + .weight(weight = 1f, fill = false) ) { Text( - buildAnnotatedString { - mockDataField.forEach { - withStyle(style = paragraphStyle) { - append(bullet) - append("\t\t") - append(it) + buildAnnotatedString { + mockDataField.forEach { + withStyle(style = paragraphStyle) { + append(bullet) + append("\t\t") + append(it) + } } - } - }, + }, ) } Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 12.dp) - .navigationBarsPadding(), - horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = + Modifier.fillMaxWidth().padding(vertical = 12.dp).navigationBarsPadding(), + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Button( - onClick = { - onCancel() - }, - shape = RoundedCornerShape(6.dp), - colors = ButtonDefaults.buttonColors( - containerColor = Color.Transparent, - contentColor = ColorStone950, - ), - modifier = Modifier - .fillMaxWidth() - .border( - width = 1.dp, - color = ColorStone300, - shape = RoundedCornerShape(6.dp) - ) - .weight(1f) + onClick = { onCancel() }, + shape = RoundedCornerShape(6.dp), + colors = + ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = ColorStone950, + ), + modifier = + Modifier.fillMaxWidth() + .border( + width = 1.dp, + color = ColorStone300, + shape = RoundedCornerShape(6.dp) + ) + .weight(1f) ) { Text( - text = "Cancel", - fontFamily = Inter, - fontWeight = FontWeight.SemiBold, - color = ColorStone950, + text = "Cancel", + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + color = ColorStone950, ) } Button( - onClick = { - onContinue() - }, - shape = RoundedCornerShape(6.dp), - colors = ButtonDefaults.buttonColors( - containerColor = ColorEmerald900 - ), - modifier = Modifier - .fillMaxWidth() - .background( - color = ColorEmerald900, - shape = RoundedCornerShape(6.dp), - ) - .weight(1f) + onClick = { onContinue() }, + shape = RoundedCornerShape(6.dp), + colors = ButtonDefaults.buttonColors(containerColor = ColorEmerald900), + modifier = + Modifier.fillMaxWidth() + .background( + color = ColorEmerald900, + shape = RoundedCornerShape(6.dp), + ) + .weight(1f) ) { Text( - text = "Approve", - fontFamily = Inter, - fontWeight = FontWeight.SemiBold, - color = ColorBase50, + text = "Approve", + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + color = ColorBase50, ) } } @@ -294,12 +322,12 @@ fun DataFieldSelector( @Composable fun CredentialSelector( - credentials: List, - credentialClaims: Map, - getRequestedFields: (ParsedCredential) -> List, - onContinue: (List) -> Unit, - onCancel: () -> Unit, - allowMultiple: Boolean = false + credentials: List, + credentialClaims: Map, + getRequestedFields: (ParsedCredential) -> List, + onContinue: (List) -> Unit, + onCancel: () -> Unit, + allowMultiple: Boolean = false ) { val selectedCredentials = remember { mutableStateListOf() } @@ -321,8 +349,7 @@ fun CredentialSelector( credentialClaims[credential.id()]?.getString("name").let { return it.toString() } - } catch (_: Exception) { - } + } catch (_: Exception) {} try { credentialClaims[credential.id()]?.getJSONArray("type").let { @@ -333,124 +360,117 @@ fun CredentialSelector( } return "" } - } catch (_: Exception) { - } + } catch (_: Exception) {} return "" } - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp) - .padding(top = 48.dp) - ) { + Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp).padding(top = 48.dp)) { Text( - text = "Select the credential${if (allowMultiple) "(s)" else ""} to share", - fontFamily = Inter, - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - color = ColorStone950, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp), - textAlign = TextAlign.Center + text = "Select the credential${if (allowMultiple) "(s)" else ""} to share", + fontFamily = Inter, + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + color = ColorStone950, + modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp), + textAlign = TextAlign.Center ) if (allowMultiple) { Text( - text = "Select All", - fontFamily = Inter, - fontWeight = FontWeight.Normal, - fontSize = 15.sp, - color = ColorBlue600, - modifier = Modifier.clickable { - // TODO: implement select all - } + text = "Select All", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 15.sp, + color = ColorBlue600, + modifier = + Modifier.clickable { + // TODO: implement select all + } ) } Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .weight(weight = 1f, fill = false) + modifier = + Modifier.fillMaxSize() + .verticalScroll(rememberScrollState()) + .weight(weight = 1f, fill = false) ) { credentials.forEach { credential -> CredentialSelectorItem( - credential = credential, - requestedFields = getRequestedFields(credential), - getCredentialTitle = { cred -> getCredentialTitle(cred) }, - isChecked = credential in selectedCredentials, - selectCredential = { cred -> selectCredential(cred) }, - removeCredential = { cred -> removeCredential(cred) }, + credential = credential, + requestedFields = getRequestedFields(credential), + getCredentialTitle = { cred -> getCredentialTitle(cred) }, + isChecked = credential in selectedCredentials, + selectCredential = { cred -> selectCredential(cred) }, + removeCredential = { cred -> removeCredential(cred) }, ) } } Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 12.dp) - .navigationBarsPadding(), - horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = + Modifier.fillMaxWidth().padding(vertical = 12.dp).navigationBarsPadding(), + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Button( - onClick = { - onCancel() - }, - shape = RoundedCornerShape(6.dp), - colors = ButtonDefaults.buttonColors( - containerColor = Color.Transparent, - contentColor = ColorStone950, - ), - modifier = Modifier - .fillMaxWidth() - .border( - width = 1.dp, - color = ColorStone300, - shape = RoundedCornerShape(6.dp) - ) - .weight(1f) + onClick = { onCancel() }, + shape = RoundedCornerShape(6.dp), + colors = + ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = ColorStone950, + ), + modifier = + Modifier.fillMaxWidth() + .border( + width = 1.dp, + color = ColorStone300, + shape = RoundedCornerShape(6.dp) + ) + .weight(1f) ) { Text( - text = "Cancel", - fontFamily = Inter, - fontWeight = FontWeight.SemiBold, - color = ColorStone950, + text = "Cancel", + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + color = ColorStone950, ) } Button( - onClick = { - if (selectedCredentials.isNotEmpty()) { - onContinue(selectedCredentials) - } - }, - shape = RoundedCornerShape(6.dp), - colors = ButtonDefaults.buttonColors( - containerColor = if (selectedCredentials.isNotEmpty()) { - ColorStone600 - } else { - Color.Gray - } - ), - modifier = Modifier - .fillMaxWidth() - .background( - color = if (selectedCredentials.isNotEmpty()) { - ColorStone600 - } else { - Color.Gray - }, - shape = RoundedCornerShape(6.dp), - ) - .weight(1f) + onClick = { + if (selectedCredentials.isNotEmpty()) { + onContinue(selectedCredentials) + } + }, + shape = RoundedCornerShape(6.dp), + colors = + ButtonDefaults.buttonColors( + containerColor = + if (selectedCredentials.isNotEmpty()) { + ColorStone600 + } else { + Color.Gray + } + ), + modifier = + Modifier.fillMaxWidth() + .background( + color = + if (selectedCredentials.isNotEmpty()) { + ColorStone600 + } else { + Color.Gray + }, + shape = RoundedCornerShape(6.dp), + ) + .weight(1f) ) { Text( - text = "Continue", - fontFamily = Inter, - fontWeight = FontWeight.SemiBold, - color = ColorBase50, + text = "Continue", + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + color = ColorBase50, ) } } @@ -459,90 +479,85 @@ fun CredentialSelector( @Composable fun CredentialSelectorItem( - credential: ParsedCredential, - requestedFields: List, - getCredentialTitle: (ParsedCredential) -> String, - isChecked: Boolean, - selectCredential: (ParsedCredential) -> Unit, - removeCredential: (ParsedCredential) -> Unit + credential: ParsedCredential, + requestedFields: List, + getCredentialTitle: (ParsedCredential) -> String, + isChecked: Boolean, + selectCredential: (ParsedCredential) -> Unit, + removeCredential: (ParsedCredential) -> Unit ) { var expanded by remember { mutableStateOf(false) } val bullet = "\u2022" val paragraphStyle = ParagraphStyle(textIndent = TextIndent(restLine = 12.sp)) - val mockDataField = requestedFields.map { field -> - field.name()?.replaceFirstChar(Char::titlecase) ?: "" - } + val mockDataField = + requestedFields.map { field -> field.name()?.replaceFirstChar(Char::titlecase) ?: "" } Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - .border( - width = 1.dp, - color = ColorBase300, - shape = RoundedCornerShape(8.dp) - ) + modifier = + Modifier.fillMaxWidth() + .padding(vertical = 8.dp) + .border( + width = 1.dp, + color = ColorBase300, + shape = RoundedCornerShape(8.dp) + ) ) { Row( - modifier = Modifier - .fillMaxWidth() - .padding(end = 8.dp) - .padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically + modifier = Modifier.fillMaxWidth().padding(end = 8.dp).padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically ) { Checkbox( - checked = isChecked, - onCheckedChange = { isChecked -> - if (isChecked) { - selectCredential(credential) - } else { - removeCredential(credential) - } - }, - colors = CheckboxDefaults.colors( - checkedColor = ColorBlue600, - uncheckedColor = ColorStone300 - ) + checked = isChecked, + onCheckedChange = { isChecked -> + if (isChecked) { + selectCredential(credential) + } else { + removeCredential(credential) + } + }, + colors = + CheckboxDefaults.colors( + checkedColor = ColorBlue600, + uncheckedColor = ColorStone300 + ) ) Text( - text = getCredentialTitle(credential), - fontFamily = Inter, - fontWeight = FontWeight.SemiBold, - fontSize = 18.sp, - color = ColorStone950, - modifier = Modifier.weight(1f) + text = getCredentialTitle(credential), + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp, + color = ColorStone950, + modifier = Modifier.weight(1f) ) if (expanded) { Image( - painter = painterResource(id = R.drawable.collapse), - contentDescription = stringResource(id = R.string.collapse), - modifier = Modifier.clickable { expanded = false } + painter = painterResource(id = R.drawable.collapse), + contentDescription = stringResource(id = R.string.collapse), + modifier = Modifier.clickable { expanded = false } ) } else { Image( - painter = painterResource(id = R.drawable.expand), - contentDescription = stringResource(id = R.string.expand), - modifier = Modifier.clickable { expanded = true } + painter = painterResource(id = R.drawable.expand), + contentDescription = stringResource(id = R.string.expand), + modifier = Modifier.clickable { expanded = true } ) } - } if (expanded) { Text( - buildAnnotatedString { - mockDataField.forEach { - withStyle(style = paragraphStyle) { - append(bullet) - append("\t\t") - append(it) + buildAnnotatedString { + mockDataField.forEach { + withStyle(style = paragraphStyle) { + append(bullet) + append("\t\t") + append(it) + } } - } - }, - modifier = Modifier.padding(16.dp) + }, + modifier = Modifier.padding(16.dp) ) } } - -} \ No newline at end of file +} diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt index 5161e87..322a17a 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt @@ -18,8 +18,13 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -30,6 +35,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController +import com.google.accompanist.swiperefresh.SwipeRefresh +import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.spruceid.mobilesdkexample.LoadingView import com.spruceid.mobilesdkexample.R import com.spruceid.mobilesdkexample.credentials.GenericCredentialItem @@ -40,12 +47,15 @@ import com.spruceid.mobilesdkexample.ui.theme.ColorStone950 import com.spruceid.mobilesdkexample.ui.theme.Inter import com.spruceid.mobilesdkexample.utils.getFileContent import com.spruceid.mobilesdkexample.viewmodels.CredentialPacksViewModel +import com.spruceid.mobilesdkexample.viewmodels.StatusListViewModel +import kotlinx.coroutines.launch import com.spruceid.mobilesdkexample.viewmodels.HelpersViewModel @Composable fun WalletHomeView( navController: NavController, credentialPacksViewModel: CredentialPacksViewModel, + statusListViewModel: StatusListViewModel, helpersViewModel: HelpersViewModel ) { Column( @@ -56,6 +66,7 @@ fun WalletHomeView( WalletHomeHeader(navController = navController) WalletHomeBody( credentialPacksViewModel = credentialPacksViewModel, + statusListViewModel = statusListViewModel, helpersViewModel = helpersViewModel ) } @@ -119,14 +130,36 @@ fun WalletHomeHeader(navController: NavController) { @Composable fun WalletHomeBody( credentialPacksViewModel: CredentialPacksViewModel, - helpersViewModel: HelpersViewModel + helpersViewModel: HelpersViewModel, + statusListViewModel: StatusListViewModel ) { + val scope = rememberCoroutineScope() val credentialPacks by credentialPacksViewModel.credentialPacks.collectAsState() val loadingCredentialPacks by credentialPacksViewModel.loading.collectAsState() + var isRefreshing by remember { mutableStateOf(false) } + + LaunchedEffect(credentialPacks) { + if (credentialPacks.isNotEmpty()) { + statusListViewModel.getStatusLists(credentialPacks) + } + } if (!loadingCredentialPacks) { if (credentialPacks.isNotEmpty()) { - Box(modifier = Modifier.fillMaxSize()) { + SwipeRefresh( + state = rememberSwipeRefreshState(isRefreshing), + onRefresh = { + isRefreshing = true + scope.launch { + if (credentialPacks.isNotEmpty()) { + statusListViewModel.getStatusLists(credentialPacks) + } + isRefreshing = false + } + }, + modifier = Modifier + .fillMaxSize() + ) { Column( Modifier .fillMaxWidth() @@ -136,6 +169,7 @@ fun WalletHomeBody( credentialPacks.forEach { credentialPack -> GenericCredentialItem( credentialPack = credentialPack, + statusListViewModel = statusListViewModel, onDelete = { credentialPacksViewModel.deleteCredentialPack(credentialPack) }, diff --git a/example/src/main/res/drawable-hdpi/valid.png b/example/src/main/res/drawable-hdpi/valid.png deleted file mode 100644 index 90da610..0000000 Binary files a/example/src/main/res/drawable-hdpi/valid.png and /dev/null differ diff --git a/example/src/main/res/drawable-hdpi/valid_check.png b/example/src/main/res/drawable-hdpi/valid_check.png deleted file mode 100644 index ca277b1..0000000 Binary files a/example/src/main/res/drawable-hdpi/valid_check.png and /dev/null differ diff --git a/example/src/main/res/drawable-mdpi/valid.png b/example/src/main/res/drawable-mdpi/valid.png deleted file mode 100644 index 8c14918..0000000 Binary files a/example/src/main/res/drawable-mdpi/valid.png and /dev/null differ diff --git a/example/src/main/res/drawable-mdpi/valid_check.png b/example/src/main/res/drawable-mdpi/valid_check.png deleted file mode 100644 index 19ef988..0000000 Binary files a/example/src/main/res/drawable-mdpi/valid_check.png and /dev/null differ diff --git a/example/src/main/res/drawable-xhdpi/valid.png b/example/src/main/res/drawable-xhdpi/valid.png deleted file mode 100644 index 61a8a55..0000000 Binary files a/example/src/main/res/drawable-xhdpi/valid.png and /dev/null differ diff --git a/example/src/main/res/drawable-xhdpi/valid_check.png b/example/src/main/res/drawable-xhdpi/valid_check.png deleted file mode 100644 index f45b035..0000000 Binary files a/example/src/main/res/drawable-xhdpi/valid_check.png and /dev/null differ diff --git a/example/src/main/res/drawable-xxhdpi/valid.png b/example/src/main/res/drawable-xxhdpi/valid.png deleted file mode 100644 index 997cbf9..0000000 Binary files a/example/src/main/res/drawable-xxhdpi/valid.png and /dev/null differ diff --git a/example/src/main/res/drawable-xxhdpi/valid_check.png b/example/src/main/res/drawable-xxhdpi/valid_check.png deleted file mode 100644 index ea07cd5..0000000 Binary files a/example/src/main/res/drawable-xxhdpi/valid_check.png and /dev/null differ diff --git a/example/src/main/res/drawable-xxxhdpi/valid.png b/example/src/main/res/drawable-xxxhdpi/valid.png deleted file mode 100644 index 513fb14..0000000 Binary files a/example/src/main/res/drawable-xxxhdpi/valid.png and /dev/null differ diff --git a/example/src/main/res/drawable-xxxhdpi/valid_check.png b/example/src/main/res/drawable-xxxhdpi/valid_check.png deleted file mode 100644 index 1eb3f07..0000000 Binary files a/example/src/main/res/drawable-xxxhdpi/valid_check.png and /dev/null differ diff --git a/example/src/main/res/drawable/invalid.xml b/example/src/main/res/drawable/invalid.xml new file mode 100644 index 0000000..4b65207 --- /dev/null +++ b/example/src/main/res/drawable/invalid.xml @@ -0,0 +1,9 @@ + + + diff --git a/example/src/main/res/drawable/suspended.xml b/example/src/main/res/drawable/suspended.xml new file mode 100644 index 0000000..5f4b2b4 --- /dev/null +++ b/example/src/main/res/drawable/suspended.xml @@ -0,0 +1,9 @@ + + + diff --git a/example/src/main/res/drawable/unknown.xml b/example/src/main/res/drawable/unknown.xml new file mode 100644 index 0000000..62c8023 --- /dev/null +++ b/example/src/main/res/drawable/unknown.xml @@ -0,0 +1,12 @@ + + + + diff --git a/example/src/main/res/drawable/valid.xml b/example/src/main/res/drawable/valid.xml new file mode 100644 index 0000000..20a7f59 --- /dev/null +++ b/example/src/main/res/drawable/valid.xml @@ -0,0 +1,9 @@ + + + diff --git a/example/src/main/res/drawable/valid_check.xml b/example/src/main/res/drawable/valid_check.xml new file mode 100644 index 0000000..af47a9a --- /dev/null +++ b/example/src/main/res/drawable/valid_check.xml @@ -0,0 +1,11 @@ + + + diff --git a/example/src/main/res/values/strings.xml b/example/src/main/res/values/strings.xml index 15b373e..eb9bfff 100644 --- a/example/src/main/res/values/strings.xml +++ b/example/src/main/res/values/strings.xml @@ -11,6 +11,9 @@ Arrow right Valid credential Invalid credential + Suspended credential + Unknown credential + Revoked credential Verification Activity Log Start action No credentials added yet @@ -24,4 +27,4 @@ Settings Click to filter Export - \ No newline at end of file +