diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/HomeView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/HomeView.kt index 5b2c5b1..f3cda1c 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.HelpersViewModel import com.spruceid.mobilesdkexample.viewmodels.VerificationMethodsViewModel import com.spruceid.mobilesdkexample.wallet.WalletHomeView @@ -53,7 +54,8 @@ fun HomeView( navController: NavController, initialTab: String, verificationMethodsViewModel: VerificationMethodsViewModel, - credentialPacksViewModel: CredentialPacksViewModel + credentialPacksViewModel: CredentialPacksViewModel, + helpersViewModel: HelpersViewModel ) { var tab by remember { if (initialTab == "verifier") { @@ -76,7 +78,8 @@ fun HomeView( if (tab == HomeTabs.WALLET) { WalletHomeView( navController, - credentialPacksViewModel = credentialPacksViewModel + credentialPacksViewModel = credentialPacksViewModel, + helpersViewModel = helpersViewModel ) } else { VerifierHomeView( 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 30d9257..8265ba4 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/credentials/AddToWalletView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/credentials/AddToWalletView.kt @@ -50,7 +50,7 @@ fun AddToWalletView( val scope = rememberCoroutineScope() LaunchedEffect(Unit) { - credentialItem = credentialDisplaySelector(rawCredential, null) + credentialItem = credentialDisplaySelector(rawCredential, null, null) } fun back() { diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/credentials/CredentialOptionsDialogActions.kt b/example/src/main/java/com/spruceid/mobilesdkexample/credentials/CredentialOptionsDialogActions.kt new file mode 100644 index 0000000..2786901 --- /dev/null +++ b/example/src/main/java/com/spruceid/mobilesdkexample/credentials/CredentialOptionsDialogActions.kt @@ -0,0 +1,126 @@ +package com.spruceid.mobilesdkexample.credentials + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.spruceid.mobilesdkexample.ui.theme.ColorBlue600 +import com.spruceid.mobilesdkexample.ui.theme.ColorRose600 +import com.spruceid.mobilesdkexample.ui.theme.ColorStone950 +import com.spruceid.mobilesdkexample.ui.theme.Inter +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CredentialOptionsDialogActions( + setShowBottomSheet: (Boolean) -> Unit, + onExport: (() -> Unit)?, + onDelete: (() -> Unit)? +) { + val scope = rememberCoroutineScope() + val sheetState = rememberModalBottomSheetState() + + ModalBottomSheet( + onDismissRequest = { + setShowBottomSheet(false) + }, + sheetState = sheetState, + modifier = Modifier.navigationBarsPadding() + ) { + Text( + text = "Credential Options", + textAlign = TextAlign.Center, + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + color = ColorStone950, + modifier = Modifier + .fillMaxWidth() + ) + if (onExport != null) { + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + Button( + onClick = { + setShowBottomSheet(false) + onExport() + }, + shape = RoundedCornerShape(5.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = ColorBlue600, + ), + modifier = Modifier + .fillMaxWidth() + ) { + Text( + text = "Export", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + color = ColorBlue600, + ) + } + } + if (onDelete != null) { + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + Button( + onClick = { + setShowBottomSheet(false) + onDelete() + }, + shape = RoundedCornerShape(5.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = ColorRose600, + ), + modifier = Modifier + .fillMaxWidth() + ) { + Text( + text = "Delete", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + color = ColorRose600, + ) + } + } + + Button( + onClick = { + scope.launch { sheetState.hide() }.invokeOnCompletion { + if (!sheetState.isVisible) { + setShowBottomSheet(false) + } + } + }, + shape = RoundedCornerShape(5.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = ColorBlue600, + ), + modifier = Modifier + .fillMaxWidth() + ) { + Text( + text = "Cancel", + fontFamily = Inter, + fontWeight = FontWeight.Bold, + color = ColorBlue600, + ) + } + } +} \ 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 ab985ea..0c1291b 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/credentials/GenericCredentialItem.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/credentials/GenericCredentialItem.kt @@ -12,14 +12,10 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState @@ -27,11 +23,9 @@ import androidx.compose.runtime.Composable 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.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.painterResource @@ -49,28 +43,36 @@ import com.spruceid.mobile.sdk.ui.toCardRendering import com.spruceid.mobilesdkexample.R import com.spruceid.mobilesdkexample.ui.theme.ColorBase1 import com.spruceid.mobilesdkexample.ui.theme.ColorBase300 -import com.spruceid.mobilesdkexample.ui.theme.ColorBlue600 -import com.spruceid.mobilesdkexample.ui.theme.ColorRose600 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.addCredential import com.spruceid.mobilesdkexample.utils.splitCamelCase -import kotlinx.coroutines.launch import org.json.JSONObject class GenericCredentialItem : ICredentialView { override var credentialPack: CredentialPack private val onDelete: (() -> Unit)? + private val onExport: ((String) -> Unit)? - constructor(credentialPack: CredentialPack, onDelete: (() -> Unit)? = null) { + constructor( + credentialPack: CredentialPack, + onDelete: (() -> Unit)? = null, + onExport: ((String) -> Unit)? = null + ) { this.credentialPack = credentialPack this.onDelete = onDelete + this.onExport = onExport } - constructor(rawCredential: String, onDelete: (() -> Unit)? = null) { + constructor( + rawCredential: String, + onDelete: (() -> Unit)? = null, + onExport: ((String) -> Unit)? = null + ) { this.credentialPack = addCredential(CredentialPack(), rawCredential) this.onDelete = onDelete + this.onExport = onExport } @Composable @@ -236,11 +238,8 @@ class GenericCredentialItem : ICredentialView { ) } - @OptIn(ExperimentalMaterial3Api::class) @Composable fun listItemWithOptions() { - val sheetState = rememberModalBottomSheetState() - val scope = rememberCoroutineScope() var showBottomSheet by remember { mutableStateOf(false) } val listRendering = CardRenderingListView( @@ -303,6 +302,17 @@ class GenericCredentialItem : ICredentialView { color = ColorStone950, modifier = Modifier.padding(bottom = 8.dp) ) + if (showBottomSheet) { + CredentialOptionsDialogActions( + setShowBottomSheet = { show -> + showBottomSheet = show + }, + onDelete = onDelete, + onExport = { + onExport?.let { it(title) } + } + ) + } } }, descriptionKeys = listOf("description", "issuer"), @@ -319,72 +329,6 @@ class GenericCredentialItem : ICredentialView { credentialPack = credentialPack, rendering = listRendering.toCardRendering() ) - - if (showBottomSheet) { - ModalBottomSheet( - onDismissRequest = { - showBottomSheet = false - }, - sheetState = sheetState, - modifier = Modifier.navigationBarsPadding() - ) { - Text( - text = "Credential Options", - textAlign = TextAlign.Center, - fontFamily = Inter, - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - color = ColorStone950, - modifier = Modifier - .fillMaxWidth() - ) - HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) - Button( - onClick = { - showBottomSheet = false - onDelete?.invoke() - }, - shape = RoundedCornerShape(5.dp), - colors = ButtonDefaults.buttonColors( - containerColor = Color.Transparent, - contentColor = ColorRose600, - ), - modifier = Modifier - .fillMaxWidth() - ) { - Text( - text = "Delete", - fontFamily = Inter, - fontWeight = FontWeight.Normal, - color = ColorRose600, - ) - } - - Button( - onClick = { - scope.launch { sheetState.hide() }.invokeOnCompletion { - if (!sheetState.isVisible) { - showBottomSheet = false - } - } - }, - shape = RoundedCornerShape(5.dp), - colors = ButtonDefaults.buttonColors( - containerColor = Color.Transparent, - contentColor = ColorBlue600, - ), - modifier = Modifier - .fillMaxWidth() - ) { - Text( - text = "Cancel", - fontFamily = Inter, - fontWeight = FontWeight.Bold, - color = ColorBlue600, - ) - } - } - } } @Composable 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 96103fd..7522392 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt @@ -48,7 +48,8 @@ fun SetupNavGraph( navController, initialTab = tab, verificationMethodsViewModel = verificationMethodsViewModel, - credentialPacksViewModel = credentialPacksViewModel + credentialPacksViewModel = credentialPacksViewModel, + helpersViewModel = helpersViewModel ) } composable( 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 e79cbe3..a723c34 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt @@ -54,6 +54,7 @@ fun String.removeUnderscores() = replace("_", "") fun String.removeCommas() = replace(",", "") +fun String.removeEscaping() = replace("\\/", "/") fun String.isDate(): Boolean { return lowercase().contains("date") || @@ -120,7 +121,11 @@ fun keyPathFinder(json: Any, path: MutableList): Any { } } -fun credentialDisplaySelector(rawCredential: String, onDelete: (() -> Unit)?): ICredentialView { +fun credentialDisplaySelector( + rawCredential: String, + onDelete: (() -> Unit)?, + onExport: ((String) -> Unit)? +): ICredentialView { /* This is temporarily commented on until we define the specific AchievementCredentialItem design */ // try { // Test if it is SdJwt @@ -128,7 +133,7 @@ fun credentialDisplaySelector(rawCredential: String, onDelete: (() -> Unit)?): I // credentialPack.addSdJwt(Vcdm2SdJwt.newFromCompactSdJwt(rawCredential)) // return AchievementCredentialItem(credentialPack, onDelete) // } catch (_: Exception) { - return GenericCredentialItem(rawCredential, onDelete) + return GenericCredentialItem(rawCredential, onDelete, onExport) // } } @@ -161,3 +166,42 @@ fun addCredential(credentialPack: CredentialPack, rawCredential: String): Creden return credentialPack } + +fun getFileContent(credentialPack: CredentialPack): String { + val rawCredentials = mutableListOf() + val claims = credentialPack.findCredentialClaims(listOf()) + + credentialPack.list().forEach { parsedCredential -> + if (parsedCredential.asSdJwt() != null) { + rawCredentials.add( + envelopVerifiableSdJwtCredential( + String(parsedCredential.intoGenericForm().payload) + ) + ) + } else { + claims[parsedCredential.id()].let { + if (it != null) { + rawCredentials.add(it.toString(4).removeEscaping()) + } + } + } + } + return rawCredentials.first() +} + +fun envelopVerifiableSdJwtCredential(sdJwt: String): String { + val jsonString = """ + { + "@context": ["https://www.w3.org/ns/credentials/v2"], + "type": ["EnvelopedVerifiableCredential"], + "id": "data:application/vc+sd-jwt,$sdJwt" + } + """ + try { + val jsonObject = JSONObject(jsonString) + val prettyPrinted = jsonObject.toString(4) + return prettyPrinted.removeEscaping() + } catch (e: Exception) { + return jsonString.removeEscaping() + } +} 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 345ce99..e14e2f2 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifierCredentialSuccessView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/verifier/VerifierCredentialSuccessView.kt @@ -35,7 +35,7 @@ fun VerifierCredentialSuccessView( onClose: () -> Unit, logVerification: (String, String) -> Unit ) { - val credentialItem = credentialDisplaySelector(rawCredential, null) + val credentialItem = credentialDisplaySelector(rawCredential, null, null) var title by remember { mutableStateOf(null) } var issuer by remember { mutableStateOf(null) } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/verifiersettings/VerifierSettingsActivityLogScreen.kt b/example/src/main/java/com/spruceid/mobilesdkexample/verifiersettings/VerifierSettingsActivityLogScreen.kt index 185c511..3ea01d8 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/verifiersettings/VerifierSettingsActivityLogScreen.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/verifiersettings/VerifierSettingsActivityLogScreen.kt @@ -80,9 +80,10 @@ fun VerifierSettingsActivityLogScreen( VerifierSettingsActivityLogScreenBody( verificationActivityLogs = verificationActivityLogs, export = { logs -> - helpersViewModel.exportCSV( + helpersViewModel.exportText( verificationActivityLogsViewModel.generateVerificationActivityLogCSV(logs = logs), - "activity_logs.csv" + "activity_logs.csv", + "text/csv" ) } ) diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/HelpersViewModel.kt b/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/HelpersViewModel.kt index 5e29c48..2e8ba89 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/HelpersViewModel.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/viewmodels/HelpersViewModel.kt @@ -21,7 +21,7 @@ fun File.updateText(content: String) { } class HelpersViewModel(application: Application) : AndroidViewModel(application) { - fun exportCSV(content: String, filename: String) { + fun exportText(content: String, filename: String, fileType: String) { val app = getApplication() val file = File(app.cacheDir, filename) file.updateText(content) @@ -33,7 +33,7 @@ class HelpersViewModel(application: Application) : AndroidViewModel(application) file, ) Intent(Intent.ACTION_SEND).apply { - type = "text/csv" + type = fileType flags = Intent.FLAG_ACTIVITY_NEW_TASK addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) putExtra(Intent.EXTRA_STREAM, uri) 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 4ea2084..5161e87 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt @@ -38,12 +38,15 @@ import com.spruceid.mobilesdkexample.ui.theme.ColorBase150 import com.spruceid.mobilesdkexample.ui.theme.ColorStone400 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.HelpersViewModel @Composable fun WalletHomeView( navController: NavController, - credentialPacksViewModel: CredentialPacksViewModel + credentialPacksViewModel: CredentialPacksViewModel, + helpersViewModel: HelpersViewModel ) { Column( Modifier @@ -51,7 +54,10 @@ fun WalletHomeView( .padding(top = 20.dp) ) { WalletHomeHeader(navController = navController) - WalletHomeBody(credentialPacksViewModel = credentialPacksViewModel) + WalletHomeBody( + credentialPacksViewModel = credentialPacksViewModel, + helpersViewModel = helpersViewModel + ) } } @@ -111,7 +117,10 @@ fun WalletHomeHeader(navController: NavController) { } @Composable -fun WalletHomeBody(credentialPacksViewModel: CredentialPacksViewModel) { +fun WalletHomeBody( + credentialPacksViewModel: CredentialPacksViewModel, + helpersViewModel: HelpersViewModel +) { val credentialPacks by credentialPacksViewModel.credentialPacks.collectAsState() val loadingCredentialPacks by credentialPacksViewModel.loading.collectAsState() @@ -129,6 +138,13 @@ fun WalletHomeBody(credentialPacksViewModel: CredentialPacksViewModel) { credentialPack = credentialPack, onDelete = { credentialPacksViewModel.deleteCredentialPack(credentialPack) + }, + onExport = { credentialTitle -> + helpersViewModel.exportText( + getFileContent(credentialPack), + "$credentialTitle.json", + "text/plain" + ) } ) .credentialPreviewAndDetails()