Skip to content

Commit

Permalink
[Android] [iOS] Universal Scanner (#39)
Browse files Browse the repository at this point in the history
This implements only one button with a scanner that can handle:
- External URLs: support QR Code with external URLs by redirecting the user to the browser
- OID4VP URLs:  redirects the user to the oid4vp flow
- OID4VCI URLs: redirects the user to the oid4vci flow
  • Loading branch information
Juliano1612 authored Nov 7, 2024
1 parent 976f73b commit cd8a5c2
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}" })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,93 @@ 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
fun DispatchQRView(
navController: NavController,
) {
val scope = rememberCoroutineScope()
val uriHandler = LocalUriHandler.current

fun onRead(url: String) {
var err by remember { mutableStateOf<String?>(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
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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 {
Expand All @@ -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) {
Expand All @@ -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!!,
Expand Down
Loading

0 comments on commit cd8a5c2

Please sign in to comment.