diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/navigation/Screen.kt b/example/src/main/java/com/spruceid/mobilesdkexample/navigation/Screen.kt index 64f766e..986dcbe 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/navigation/Screen.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/navigation/Screen.kt @@ -11,7 +11,7 @@ const val ADD_VERIFICATION_METHOD_PATH = "add_verification_method" const val WALLET_SETTINGS_HOME_PATH = "wallet_settings_home" const val ADD_TO_WALLET_PATH = "add_to_wallet/{rawCredential}" const val SCAN_QR_PATH = "scan_qr" -const val OID4VCI_PATH = "oid4vci" +const val HANDLE_OID4VCI_PATH = "oid4vci/{url}" const val HANDLE_OID4VP_PATH = "oid4vp/{url}" sealed class Screen(val route: String) { @@ -26,6 +26,6 @@ sealed class Screen(val route: String) { object WalletSettingsHomeScreen : Screen(WALLET_SETTINGS_HOME_PATH) object AddToWalletScreen : Screen(ADD_TO_WALLET_PATH) object ScanQRScreen : Screen(SCAN_QR_PATH) - object OID4VCIScreen : Screen(OID4VCI_PATH) + object HandleOID4VCI : Screen(HANDLE_OID4VCI_PATH) object HandleOID4VP : Screen(HANDLE_OID4VP_PATH) } 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 8697216..2274244 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt @@ -16,8 +16,8 @@ import com.spruceid.mobilesdkexample.verifier.VerifyVCView import com.spruceid.mobilesdkexample.verifiersettings.VerifierSettingsHomeView import com.spruceid.mobilesdkexample.viewmodels.VerificationMethodsViewModel import com.spruceid.mobilesdkexample.wallet.DispatchQRView +import com.spruceid.mobilesdkexample.wallet.HandleOID4VCIView import com.spruceid.mobilesdkexample.wallet.HandleOID4VPView -import com.spruceid.mobilesdkexample.wallet.OID4VCIView import com.spruceid.mobilesdkexample.walletsettings.WalletSettingsHomeView @Composable @@ -87,8 +87,11 @@ fun SetupNavGraph( route = Screen.ScanQRScreen.route, ) { DispatchQRView(navController) } composable( - route = Screen.OID4VCIScreen.route, - ) { OID4VCIView(navController) } + route = Screen.HandleOID4VCI.route, + ) { backStackEntry -> + val url = backStackEntry.arguments?.getString("url")!! + HandleOID4VCIView(navController, url) + } composable( route = Screen.HandleOID4VP.route, deepLinks = listOf(navDeepLink { uriPattern = "openid4vp://{url}" }) diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/DispatchQRView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/DispatchQRView.kt index 73bd104..9c9bb6c 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/DispatchQRView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/DispatchQRView.kt @@ -2,17 +2,30 @@ package com.spruceid.mobilesdkexample.wallet import androidx.compose.material3.ExperimentalMaterial3Api 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.platform.LocalUriHandler import androidx.navigation.NavController import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.spruceid.mobilesdkexample.ErrorView +import com.spruceid.mobilesdkexample.LoadingView import com.spruceid.mobilesdkexample.ScanningComponent import com.spruceid.mobilesdkexample.ScanningType +import com.spruceid.mobilesdkexample.navigation.Screen import kotlinx.coroutines.launch import java.net.URLEncoder import java.nio.charset.StandardCharsets // The scheme for the OID4VP QR code. -const val OPEN_ID4VP_SCHEME = "openid4vp://" +const val OID4VP_SCHEME = "openid4vp://" +// The scheme for the OID4VCI QR code. +const val OID4VCI_SCHEME = "openid-credential-offer://" +// The schemes for HTTP/HTTPS QR code. +const val HTTP_SCHEME = "http://" +const val HTTPS_SCHEME = "https://" @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) @Composable @@ -20,23 +33,62 @@ fun DispatchQRView( navController: NavController, ) { val scope = rememberCoroutineScope() + val uriHandler = LocalUriHandler.current - fun onRead(url: String) { + var err by remember { mutableStateOf(null) } + var loading by remember { mutableStateOf(false) } + + fun back() { + navController.navigate(Screen.HomeScreen.route) { + popUpTo(0) + } + } + + fun onRead(payload: String) { + loading = true scope.launch { - if (url.startsWith(OPEN_ID4VP_SCHEME)) { - val encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8.toString()) + try { + if (payload.startsWith(OID4VP_SCHEME)) { + val encodedUrl = URLEncoder.encode(payload, StandardCharsets.UTF_8.toString()) - navController.navigate("oid4vp/$encodedUrl") { - launchSingleTop = true - restoreState = true + navController.navigate("oid4vp/$encodedUrl") { + launchSingleTop = true + restoreState = true + } + } else if (payload.startsWith(OID4VCI_SCHEME)) { + val encodedUrl = URLEncoder.encode(payload, StandardCharsets.UTF_8.toString()) + + navController.navigate("oid4vci/$encodedUrl") { + launchSingleTop = true + restoreState = true + } + } else if (payload.startsWith(HTTP_SCHEME) || payload.startsWith(HTTPS_SCHEME)) { + uriHandler.openUri(payload) + back() + } else { + err = "The QR code you have scanned is not supported. QR code payload: $payload" } + } catch (e: Exception) { + err = e.localizedMessage } } } - ScanningComponent( - navController = navController, - scanningType = ScanningType.QRCODE, - onRead = ::onRead - ) + if (err != null) { + ErrorView( + errorTitle = "Error Reading QR Code", + errorDetails = err!!, + onClose = ::back + ) + } else if (loading) { + LoadingView(loadingText = "Loading...") + } else { + ScanningComponent( + navController = navController, + scanningType = ScanningType.QRCODE, + onRead = ::onRead, + onCancel = ::back + ) + } + } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/OID4VCI.kt b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VCIView.kt similarity index 62% rename from example/src/main/java/com/spruceid/mobilesdkexample/wallet/OID4VCI.kt rename to example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VCIView.kt index d584385..f8f330f 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/OID4VCI.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VCIView.kt @@ -2,15 +2,14 @@ package com.spruceid.mobilesdkexample.wallet import android.content.Context import android.util.Base64 -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavHostController -import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.spruceid.mobile.sdk.KeyManager import com.spruceid.mobile.sdk.rs.AsyncHttpClient import com.spruceid.mobile.sdk.rs.DidMethod @@ -22,8 +21,6 @@ import com.spruceid.mobile.sdk.rs.generatePopPrepare import com.spruceid.mobilesdkexample.ErrorView import com.spruceid.mobilesdkexample.LoadingView import com.spruceid.mobilesdkexample.R -import com.spruceid.mobilesdkexample.ScanningComponent -import com.spruceid.mobilesdkexample.ScanningType import com.spruceid.mobilesdkexample.credentials.AddToWalletView import com.spruceid.mobilesdkexample.navigation.Screen import io.ktor.client.HttpClient @@ -33,14 +30,12 @@ import io.ktor.client.request.setBody import io.ktor.client.statement.readBytes import io.ktor.http.HttpMethod import io.ktor.util.toMap -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.async import org.json.JSONObject -@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) @Composable -fun OID4VCIView( +fun HandleOID4VCIView( navController: NavHostController, + url: String ) { var loading by remember { mutableStateOf(false) @@ -53,7 +48,7 @@ fun OID4VCIView( } val ctx = LocalContext.current - fun getCredential(credentialOffer: String) { + LaunchedEffect(Unit) { loading = true val client = HttpClient(CIO) val oid4vciSession = Oid4vci.newWithAsyncClient(client = object : AsyncHttpClient { @@ -72,77 +67,74 @@ fun OID4VCIView( body = res.readBytes() ) } - }) - GlobalScope.async { - try { - oid4vciSession.initiateWithOffer( - credentialOffer = credentialOffer, - clientId = "skit-demo-wallet", - redirectUrl = "https://spruceid.com" - ) + try { + oid4vciSession.initiateWithOffer( + credentialOffer = url, + clientId = "skit-demo-wallet", + redirectUrl = "https://spruceid.com" + ) - val nonce = oid4vciSession.exchangeToken() + val nonce = oid4vciSession.exchangeToken() - val metadata = oid4vciSession.getMetadata() + val metadata = oid4vciSession.getMetadata() - val keyManager = KeyManager() - keyManager.generateSigningKey(id = "reference-app/default-signing") - val jwk = keyManager.getJwk(id = "reference-app/default-signing") + val keyManager = KeyManager() + keyManager.generateSigningKey(id = "reference-app/default-signing") + val jwk = keyManager.getJwk(id = "reference-app/default-signing") - val signingInput = jwk?.let { - generatePopPrepare( - audience = metadata.issuer(), - nonce = nonce, - didMethod = DidMethod.JWK, - publicJwk = jwk, - durationInSecs = null - ) - } + val signingInput = jwk?.let { + generatePopPrepare( + audience = metadata.issuer(), + nonce = nonce, + didMethod = DidMethod.JWK, + publicJwk = jwk, + durationInSecs = null + ) + } - val signature = signingInput?.let { - keyManager.signPayload( - id = "reference-app/default-signing", - payload = signingInput - ) - } + val signature = signingInput?.let { + keyManager.signPayload( + id = "reference-app/default-signing", + payload = signingInput + ) + } - val pop = signingInput?.let { - signature?.let { - generatePopComplete( - signingInput = signingInput, - signature = Base64.encodeToString( - signature, - Base64.URL_SAFE - or Base64.NO_PADDING - or Base64.NO_WRAP - ).toByteArray() - ) - } + val pop = signingInput?.let { + signature?.let { + generatePopComplete( + signingInput = signingInput, + signature = Base64.encodeToString( + signature, + Base64.URL_SAFE + or Base64.NO_PADDING + or Base64.NO_WRAP + ).toByteArray() + ) } + } - oid4vciSession.setContextMap(getVCPlaygroundOID4VCIContext(ctx = ctx)) + oid4vciSession.setContextMap(getVCPlaygroundOID4VCIContext(ctx = ctx)) - val credentials = pop?.let { - oid4vciSession.exchangeCredential(proofsOfPossession = listOf(pop)) - } + val credentials = pop?.let { + oid4vciSession.exchangeCredential(proofsOfPossession = listOf(pop)) + } - credentials?.forEach { cred -> - cred.payload.toString(Charsets.UTF_8).let { - // Removes the renderMethod to avoid storage issues - // TODO: Remove this when replace the storage component - val json = JSONObject(it) - json.remove("renderMethod") - credential = json.toString() - } + credentials?.forEach { cred -> + cred.payload.toString(Charsets.UTF_8).let { + // Removes the renderMethod to avoid storage issues + // TODO: Optimize credential decrypt and display + val json = JSONObject(it) + json.remove("renderMethod") + credential = json.toString() } - } catch (e: Exception) { - err = e.localizedMessage - e.printStackTrace() } - loading = false + } catch (e: Exception) { + err = e.localizedMessage + e.printStackTrace() } + loading = false } if (loading) { @@ -157,14 +149,7 @@ fun OID4VCIView( } } ) - } else if (credential == null) { - ScanningComponent( - title = "Scan to Add Credential", - navController = navController, - scanningType = ScanningType.QRCODE, - onRead = ::getCredential - ) - } else { + } else if (credential != null) { AddToWalletView( navController = navController, rawCredential = credential!!, 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 c376553..8f04b64 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt @@ -17,9 +17,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf @@ -27,13 +24,10 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -42,7 +36,6 @@ import com.spruceid.mobile.sdk.CredentialPack import com.spruceid.mobilesdkexample.R import com.spruceid.mobilesdkexample.credentials.GenericCredentialItem import com.spruceid.mobilesdkexample.navigation.Screen -import com.spruceid.mobilesdkexample.ui.theme.CTAButtonBlue import com.spruceid.mobilesdkexample.ui.theme.ColorStone400 import com.spruceid.mobilesdkexample.ui.theme.Inter import com.spruceid.mobilesdkexample.ui.theme.Primary @@ -55,9 +48,7 @@ fun WalletHomeView(navController: NavController) { .padding(all = 20.dp) .padding(top = 20.dp)) { WalletHomeHeader(navController = navController) - WalletHomeBody( - navController = navController - ) + WalletHomeBody() } } @@ -81,7 +72,7 @@ fun WalletHomeHeader(navController: NavController) { .padding(start = 4.dp) .clip(shape = RoundedCornerShape(8.dp)) .background(Primary) - .clickable { navController.navigate(Screen.OID4VCIScreen.route) } + .clickable { navController.navigate(Screen.ScanQRScreen.route) } ) { Image( painter = painterResource(id = R.drawable.qrcode_scanner), @@ -117,7 +108,7 @@ fun WalletHomeHeader(navController: NavController) { } @Composable -fun WalletHomeBody(navController: NavController) { +fun WalletHomeBody() { val context = LocalContext.current val storageManager = StorageManager(context = context) val credentialPacks = remember { @@ -130,7 +121,7 @@ fun WalletHomeBody(navController: NavController) { Modifier .fillMaxWidth() .padding(top = 20.dp) - .padding(bottom = 60.dp)) { + ) { items(credentialPacks.value) { credentialPack -> GenericCredentialItem( credentialPack = credentialPack, @@ -145,35 +136,6 @@ fun WalletHomeBody(navController: NavController) { // ShareableCredentialListItems(mdocBase64 = mdocBase64) // } } - - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) { - Button( - onClick = { navController.navigate(Screen.ScanQRScreen.route) }, - modifier = Modifier.fillMaxWidth(), - colors = - ButtonDefaults.buttonColors( - containerColor = CTAButtonBlue, - contentColor = Color.White, - ) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(8.dp) - ) { - Image( - painter = painterResource(id = R.drawable.qrcode_scanner), - contentDescription = stringResource(id = R.string.qrcode_scanner), - modifier = Modifier.padding(end = 10.dp) - ) - Text( - text = "Scan to share", - fontFamily = Inter, - fontWeight = FontWeight.Normal, - fontSize = 15.sp, - ) - } - } - } } } else { Box(Modifier.fillMaxSize()) {