From a7aa2f57b77bef0283d772b2e94bc0024bfefb7d Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Mon, 14 Oct 2024 09:38:01 -0400 Subject: [PATCH] wip: oid4vp integration Signed-off-by: Ryan Tate --- MobileSdk/build.gradle.kts | 210 +++++++++--------- example/.idea/runConfigurations.xml | 13 ++ .../spruceid/mobilesdkexample/MainActivity.kt | 39 +++- .../mobilesdkexample/navigation/Screen.kt | 10 +- .../navigation/SetupNavGraph.kt | 80 +++---- .../spruceid/mobilesdkexample/utils/Utils.kt | 17 +- .../wallet/AchievementCredentialItem.kt | 4 + .../mobilesdkexample/wallet/DispatchQRView.kt | 46 ++-- .../wallet/HandleOID4VPView.kt | 201 +++++++++++++++++ .../mobilesdkexample/wallet/WalletHomeView.kt | 174 +++++++-------- 10 files changed, 511 insertions(+), 283 deletions(-) create mode 100644 example/.idea/runConfigurations.xml create mode 100644 example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VPView.kt diff --git a/MobileSdk/build.gradle.kts b/MobileSdk/build.gradle.kts index a459b9f..23ae3da 100644 --- a/MobileSdk/build.gradle.kts +++ b/MobileSdk/build.gradle.kts @@ -1,140 +1,136 @@ plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") - `maven-publish` - id("signing") - id("com.gradleup.nmcp") + id("com.android.library") + id("org.jetbrains.kotlin.android") + `maven-publish` + id("signing") + id("com.gradleup.nmcp") } publishing { - repositories { - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/spruceid/mobile-sdk-kt") - credentials { - username = System.getenv("GITHUB_ACTOR") - password = System.getenv("GITHUB_TOKEN") - } - } - } - 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" - artifactId = "mobilesdk" - version = System.getenv("VERSION") - - afterEvaluate { from(components["release"]) } - - pom { - packaging = "aar" - name.set("mobilesdk") - description.set("Android SpruceID Mobile SDK") - url.set("https://github.com/spruceid/mobile-sdk-kt") - licenses { - license { - name.set("MIT License") - url.set("https://opensource.org/license/mit/") - } - license { - name.set("Apache License, Version 2.0") - url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") - } - } - developers { - developer { - name.set("Spruce Systems, Inc.") - email.set("hello@spruceid.com") - } + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/spruceid/mobile-sdk-kt") + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } } - scm { - url.set(pom.url.get()) - connection.set("scm:git:${url.get()}.git") - developerConnection.set("scm:git:${url.get()}.git") + } + publications { + // Creates a Maven publication called "release". + create("release") { + groupId = "com.spruceid.mobile.sdk" + artifactId = "mobilesdk" + version = System.getenv("VERSION") + + afterEvaluate { from(components["release"]) } + + pom { + packaging = "aar" + name.set("mobilesdk") + description.set("Android SpruceID Mobile SDK") + url.set("https://github.com/spruceid/mobile-sdk-kt") + licenses { + license { + name.set("MIT License") + url.set("https://opensource.org/license/mit/") + } + license { + name.set("Apache License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + developers { + developer { + name.set("Spruce Systems, Inc.") + email.set("hello@spruceid.com") + } + } + scm { + url.set(pom.url.get()) + connection.set("scm:git:${url.get()}.git") + developerConnection.set("scm:git:${url.get()}.git") + } + } } - } } - } } signing { - useGpgCmd() - sign(publishing.publications["release"]) + useGpgCmd() + sign(publishing.publications["release"]) } nmcp { - afterEvaluate { - publish("release") { - username = System.getenv("MAVEN_USERNAME") - password = System.getenv("MAVEN_PASSWORD") - publicationType = "AUTOMATIC" + afterEvaluate { + publish("release") { + username = System.getenv("MAVEN_USERNAME") + password = System.getenv("MAVEN_PASSWORD") + publicationType = "AUTOMATIC" + } } - } } android { namespace = "com.spruceid.mobile.sdk" compileSdk = 35 - defaultConfig { - minSdk = 26 + defaultConfig { + minSdk = 26 - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } - buildTypes { - release { - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } - kotlinOptions { jvmTarget = "1.8" } + kotlinOptions { jvmTarget = "1.8" } - buildFeatures { - compose = true - viewBinding = true - } + buildFeatures { + compose = true + viewBinding = true + } - composeOptions { kotlinCompilerExtensionVersion = "1.5.11" } + composeOptions { kotlinCompilerExtensionVersion = "1.5.11" } - publishing { - singleVariant("release") { - withSourcesJar() - withJavadocJar() + publishing { + singleVariant("release") { + withSourcesJar() + withJavadocJar() + } } - } } dependencies { - api("com.spruceid.mobile.sdk.rs:mobilesdkrs:0.0.36") - //noinspection GradleCompatible - implementation("com.android.support:appcompat-v7:28.0.0") - /* Begin UI dependencies */ - implementation("androidx.compose.material3:material3:1.2.1") - implementation("androidx.camera:camera-camera2:1.3.2") - implementation("androidx.camera:camera-lifecycle:1.3.2") - implementation("androidx.camera:camera-view:1.3.2") - implementation("com.google.zxing:core:3.5.1") - implementation("com.google.accompanist:accompanist-permissions:0.34.0") - implementation("androidx.camera:camera-mlkit-vision:1.3.0-alpha06") - implementation("com.google.android.gms:play-services-mlkit-text-recognition:19.0.0") - /* End UI dependencies */ - implementation("androidx.datastore:datastore-preferences:1.1.1") - testImplementation("junit:junit:4.13.2") - androidTestImplementation("com.android.support.test:runner:1.0.2") - androidTestImplementation("com.android.support.test.espresso:espresso-core:3.0.2") + api("com.spruceid.mobile.sdk.rs:mobilesdkrs:0.0.99c") + //noinspection GradleCompatible + implementation("com.android.support:appcompat-v7:28.0.0") + /* Begin UI dependencies */ + implementation("androidx.compose.material3:material3:1.2.1") + implementation("androidx.camera:camera-camera2:1.3.2") + implementation("androidx.camera:camera-lifecycle:1.3.2") + implementation("androidx.camera:camera-view:1.3.2") + implementation("com.google.zxing:core:3.5.1") + implementation("com.google.accompanist:accompanist-permissions:0.34.0") + implementation("androidx.camera:camera-mlkit-vision:1.3.0-alpha06") + implementation("com.google.android.gms:play-services-mlkit-text-recognition:19.0.0") + /* End UI dependencies */ + implementation("androidx.datastore:datastore-preferences:1.1.1") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("com.android.support.test:runner:1.0.2") + androidTestImplementation("com.android.support.test.espresso:espresso-core:3.0.2") } diff --git a/example/.idea/runConfigurations.xml b/example/.idea/runConfigurations.xml new file mode 100644 index 0000000..931b96c --- /dev/null +++ b/example/.idea/runConfigurations.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt b/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt index 92afdbd..45faf37 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/MainActivity.kt @@ -9,7 +9,9 @@ import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier +import androidx.lifecycle.coroutineScope import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.spruceid.mobilesdkexample.db.AppDatabase @@ -19,6 +21,11 @@ import com.spruceid.mobilesdkexample.ui.theme.Bg import com.spruceid.mobilesdkexample.ui.theme.MobileSdkTheme import com.spruceid.mobilesdkexample.viewmodels.IRawCredentialsViewModel import com.spruceid.mobilesdkexample.viewmodels.RawCredentialsViewModelFactory +import kotlinx.coroutines.launch +import com.spruceid.mobile.sdk.KeyManager +import com.spruceid.mobilesdkexample.utils.exampleSdJwt + +const val DEFAULT_KEY_ID = "key-1" class MainActivity : ComponentActivity() { private lateinit var navController: NavHostController @@ -28,7 +35,11 @@ class MainActivity : ComponentActivity() { val deepLinkUri: Uri? = intent.data if (deepLinkUri != null) { - // @TODO: integrate with the OID4VP flow + // Remove? TBD + if (deepLinkUri.scheme == "oid4vp://") { + // NOTE: See DispatchQRView.kt for handling OID4VP QR code scanning, + // and credential selection. + } } enableEdgeToEdge() @@ -44,6 +55,32 @@ class MainActivity : ComponentActivity() { val credentialsViewModel: IRawCredentialsViewModel by viewModels { RawCredentialsViewModelFactory((application as MainApplication).rawCredentialsRepository) } + + // Insert a raw credential into the rawCredentialsRepository, + // using a suspend / async method. + LaunchedEffect(credentialsViewModel) { + lifecycle.coroutineScope.launch { + // Setup a default keyId for the RequestSigner. + // Check the key manager if the key exists, if not, create it. + val km = KeyManager() + + if (!km.keyExists(DEFAULT_KEY_ID)) { + // Key does not exist, create it. + km.generateSigningKey(DEFAULT_KEY_ID) + } + + +// // Clear the raw credentials table. +// credentialsViewModel.deleteAllRawCredentials() +// // Load the exampleSdJwt into the raw credentials table. +// credentialsViewModel.saveRawCredential( +// com.spruceid.mobilesdkexample.db.RawCredentials( +// rawCredential = exampleSdJwt +// ) +// ) + } + } + SetupNavGraph(navController, credentialsViewModel) } } 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 278dce4..54e6161 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/navigation/Screen.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/navigation/Screen.kt @@ -7,10 +7,9 @@ const val VERIFY_VC_PATH = "verify_vc" const val VERIFIER_SETTINGS_HOME_PATH = "verifier_settings_home" const val WALLET_SETTINGS_HOME_PATH = "wallet_settings_home" const val ADD_TO_WALLET_PATH = "add_to_wallet/{rawCredential}" -const val OID4VP_PATH = "oid4vp/{params}" +const val SCAN_QR_PATH = "scan_qr" const val OID4VCI_PATH = "oid4vci" - - +const val HANDLE_OID4VP_PATH = "oid4vp/{url}" sealed class Screen(val route: String) { object HomeScreen : Screen(HOME_SCREEN_PATH) @@ -20,6 +19,7 @@ sealed class Screen(val route: String) { object VerifierSettingsHomeScreen : Screen(VERIFIER_SETTINGS_HOME_PATH) object WalletSettingsHomeScreen : Screen(WALLET_SETTINGS_HOME_PATH) object AddToWalletScreen : Screen(ADD_TO_WALLET_PATH) - object ScanQRScreen : Screen(OID4VP_PATH) + object ScanQRScreen : Screen(SCAN_QR_PATH) object OID4VCIScreen : Screen(OID4VCI_PATH) -} \ No newline at end of file + 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 a348c4c..9814207 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/navigation/SetupNavGraph.kt @@ -1,6 +1,5 @@ package com.spruceid.mobilesdkexample.navigation -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost @@ -13,74 +12,55 @@ import com.spruceid.mobilesdkexample.verifier.VerifyVCView import com.spruceid.mobilesdkexample.verifiersettings.VerifierSettingsHomeView import com.spruceid.mobilesdkexample.viewmodels.IRawCredentialsViewModel import com.spruceid.mobilesdkexample.wallet.AddToWalletView +import com.spruceid.mobilesdkexample.wallet.DispatchQRView +import com.spruceid.mobilesdkexample.wallet.HandleOID4VPView import com.spruceid.mobilesdkexample.wallet.OID4VCIView import com.spruceid.mobilesdkexample.walletsettings.WalletSettingsHomeView -import com.spruceid.mobilesdkexample.wallet.DispatchQRView @Composable fun SetupNavGraph( - navController: NavHostController, - rawCredentialsViewModel: IRawCredentialsViewModel + navController: NavHostController, + rawCredentialsViewModel: IRawCredentialsViewModel ) { - NavHost( - navController = navController, - startDestination = Screen.HomeScreen.route - ) { + NavHost(navController = navController, startDestination = Screen.HomeScreen.route) { composable( - route = Screen.HomeScreen.route, - ) { - HomeView(navController, rawCredentialsViewModel) - } + route = Screen.HomeScreen.route, + ) { HomeView(navController, rawCredentialsViewModel) } composable( - route = Screen.VerifyDLScreen.route, - ) { - VerifyDLView(navController) - } + route = Screen.VerifyDLScreen.route, + ) { VerifyDLView(navController) } composable( - route = Screen.VerifyEAScreen.route, - ) { - VerifyEAView(navController) - } + route = Screen.VerifyEAScreen.route, + ) { VerifyEAView(navController) } composable( - route = Screen.VerifyVCScreen.route, - ) { - VerifyVCView(navController) - } + route = Screen.VerifyVCScreen.route, + ) { VerifyVCView(navController) } composable( - route = Screen.VerifierSettingsHomeScreen.route, - ) { - VerifierSettingsHomeView(navController) - } + route = Screen.VerifierSettingsHomeScreen.route, + ) { VerifierSettingsHomeView(navController) } composable( - route = Screen.WalletSettingsHomeScreen.route, - ) { - WalletSettingsHomeView(navController, rawCredentialsViewModel) - } + route = Screen.WalletSettingsHomeScreen.route, + ) { WalletSettingsHomeView(navController, rawCredentialsViewModel) } composable( - route = Screen.AddToWalletScreen.route, - deepLinks = listOf( - navDeepLink { - uriPattern = "spruceid://?sd-jwt={rawCredential}" - } - ) + route = Screen.AddToWalletScreen.route, + deepLinks = + listOf(navDeepLink { uriPattern = "spruceid://?sd-jwt={rawCredential}" }) ) { backStackEntry -> val rawCredential = backStackEntry.arguments?.getString("rawCredential")!! AddToWalletView(navController, rawCredential, rawCredentialsViewModel) } composable( - route = Screen.ScanQRScreen.route, - deepLinks = listOf( - navDeepLink { - uriPattern = "oid4vp://{params}" - } - ) - ) { - DispatchQRView(navController) - } + route = Screen.ScanQRScreen.route, + ) { DispatchQRView(navController) } composable( - route = Screen.OID4VCIScreen.route, - ) { - OID4VCIView(navController) + route = Screen.OID4VCIScreen.route, + ) { OID4VCIView(navController) } + composable( + route = Screen.HandleOID4VP.route, + deepLinks = listOf(navDeepLink { uriPattern = "oid4vp://{url}" }) + ) { backStackEntry -> + val url = backStackEntry.arguments?.getString("url")!! + HandleOID4VPView(navController, rawCredentialsViewModel, url) } } } 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 b67c0a1..8d9dac4 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/utils/Utils.kt @@ -1,9 +1,18 @@ package com.spruceid.mobilesdkexample.utils -val ed25519_2020_10_18 = "{\"kty\":\"OKP\",\"crv\":\"Ed25519\",\"x\":\"G80iskrv_nE69qbGLSpeOHJgmV4MKIzsy5l5iT6pCww\",\"d\":\"39Ev8-k-jkKunJyFWog3k0OwgPjnKv_qwLhfqXdAXTY\"}" - -val keyPEM = "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEAqKZdZQgPVtjlEB\nfz2ItHG8oXIONenOxRePtqOQ42yhRANCAATA43gI2Ib8+qKK4YEOfNCRiNOhyHaC\nLgAvKdhHS+y6wpG3oJ2xudXagzKKbcfvUda4x0j8zR1/oD56mpm85GbO\n-----END PRIVATE KEY-----\n-----BEGIN CERTIFICATE-----\nMIICgDCCAiWgAwIBAgIUTp04dh8m8Vxa/hX5LmTvjSWrAS8wCgYIKoZIzj0EAwIw\ngZQxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3\nIFlvcmsxEjAQBgNVBAoMCVNwcnVjZSBJRDESMBAGA1UECwwJU3BydWNlIElkMRIw\nEAYDVQQDDAlTcHJ1Y2UgSUQxIzAhBgkqhkiG9w0BCQEWFGNvbnRhY3RAc3BydWNl\naWQuY29tMB4XDTI0MDIxMjE2NTEwMVoXDTI1MDIxMTE2NTEwMVowgZQxCzAJBgNV\nBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3IFlvcmsxEjAQ\nBgNVBAoMCVNwcnVjZSBJRDESMBAGA1UECwwJU3BydWNlIElkMRIwEAYDVQQDDAlT\ncHJ1Y2UgSUQxIzAhBgkqhkiG9w0BCQEWFGNvbnRhY3RAc3BydWNlaWQuY29tMFkw\nEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwON4CNiG/PqiiuGBDnzQkYjToch2gi4A\nLynYR0vsusKRt6CdsbnV2oMyim3H71HWuMdI/M0df6A+epqZvORmzqNTMFEwHQYD\nVR0OBBYEFPbjKnGAa0aSXw0oe4KfHdN5M1ssMB8GA1UdIwQYMBaAFPbjKnGAa0aS\nXw0oe4KfHdN5M1ssMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSQAwRgIh\nAO2msc7LSdakGcw3q7DxEySqzepr+LeWWNvPbQypQxd8AiEAj7dVI3V00gq3K3OU\nCbkeKnYiGtVCZnXnR/MW91mPeGE=\n-----END CERTIFICATE-----" +val ed25519_2020_10_18 = + "{\"kty\":\"OKP\",\"crv\":\"Ed25519\",\"x\":\"G80iskrv_nE69qbGLSpeOHJgmV4MKIzsy5l5iT6pCww\",\"d\":\"39Ev8-k-jkKunJyFWog3k0OwgPjnKv_qwLhfqXdAXTY\"}" +val keyPEM = + "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEAqKZdZQgPVtjlEB\nfz2ItHG8oXIONenOxRePtqOQ42yhRANCAATA43gI2Ib8+qKK4YEOfNCRiNOhyHaC\nLgAvKdhHS+y6wpG3oJ2xudXagzKKbcfvUda4x0j8zR1/oD56mpm85GbO\n-----END PRIVATE KEY-----\n-----BEGIN CERTIFICATE-----\nMIICgDCCAiWgAwIBAgIUTp04dh8m8Vxa/hX5LmTvjSWrAS8wCgYIKoZIzj0EAwIw\ngZQxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3\nIFlvcmsxEjAQBgNVBAoMCVNwcnVjZSBJRDESMBAGA1UECwwJU3BydWNlIElkMRIw\nEAYDVQQDDAlTcHJ1Y2UgSUQxIzAhBgkqhkiG9w0BCQEWFGNvbnRhY3RAc3BydWNl\naWQuY29tMB4XDTI0MDIxMjE2NTEwMVoXDTI1MDIxMTE2NTEwMVowgZQxCzAJBgNV\nBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3IFlvcmsxEjAQ\nBgNVBAoMCVNwcnVjZSBJRDESMBAGA1UECwwJU3BydWNlIElkMRIwEAYDVQQDDAlT\ncHJ1Y2UgSUQxIzAhBgkqhkiG9w0BCQEWFGNvbnRhY3RAc3BydWNlaWQuY29tMFkw\nEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwON4CNiG/PqiiuGBDnzQkYjToch2gi4A\nLynYR0vsusKRt6CdsbnV2oMyim3H71HWuMdI/M0df6A+epqZvORmzqNTMFEwHQYD\nVR0OBBYEFPbjKnGAa0aSXw0oe4KfHdN5M1ssMB8GA1UdIwQYMBaAFPbjKnGAa0aS\nXw0oe4KfHdN5M1ssMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSQAwRgIh\nAO2msc7LSdakGcw3q7DxEySqzepr+LeWWNvPbQypQxd8AiEAj7dVI3V00gq3K3OU\nCbkeKnYiGtVCZnXnR/MW91mPeGE=\n-----END CERTIFICATE-----" val keyBase64 = - "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEAqKZdZQgPVtjlEBfz2ItHG8oXIONenOxRePtqOQ42yhRANCAATA43gI2Ib8+qKK4YEOfNCRiNOhyHaCLgAvKdhHS+y6wpG3oJ2xudXagzKKbcfvUda4x0j8zR1/oD56mpm85GbO" \ No newline at end of file + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEAqKZdZQgPVtjlEBfz2ItHG8oXIONenOxRePtqOQ42yhRANCAATA43gI2Ib8+qKK4YEOfNCRiNOhyHaCLgAvKdhHS+y6wpG3oJ2xudXagzKKbcfvUda4x0j8zR1/oD56mpm85GbO" + +// const val exampleSdJwt = +// "eyJhbGciOiJFUzI1NiJ9.eyJfc2RfYWxnIjoic2hhMjU2IiwiY3JlZGVudGlhbFN1YmplY3QiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnL25zL2NyZWRlbnRpYWxzL3YyIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjMuanNvbiJdLCJhd2FyZGVkRGF0ZSI6IjIwMjQtMDktMjNUMTg6MTI6MTIrMDAwMCIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkZW50aXR5IjpbeyJoYXNoZWQiOmZhbHNlLCJpZGVudGl0eUhhc2giOiJKb2huIFNtaXRoIiwiaWRlbnRpdHlUeXBlIjoibmFtZSIsInNhbHQiOiJub3QtdXNlZCIsInR5cGUiOiJJZGVudGl0eU9iamVjdCJ9LHsiaGFzaGVkIjpmYWxzZSwiaWRlbnRpdHlIYXNoIjoiam9obi5zbWl0aEBleGFtcGxlLmNvbSIsImlkZW50aXR5VHlwZSI6ImVtYWlsQWRkcmVzcyIsInNhbHQiOiJub3QtdXNlZCIsInR5cGUiOiJJZGVudGl0eU9iamVjdCJ9XSwiYWNoaWV2ZW1lbnQiOnsibmFtZSI6IkNvbG9yYWRvRldEIFRlYW0gTWVtYmVyc2hpcCIsInR5cGUiOiJBY2hpZXZlbWVudCJ9fSwiaXNzdWVyIjp7ImlkIjoiZGlkOmp3azpleUpoYkdjaU9pSkZVekkxTmlJc0ltTnlkaUk2SWxBdE1qVTJJaXdpYTNSNUlqb2lSVU1pTENKNElqb2liV0pVTTJkcU9XRnZPR051UzI4ME0wcHJjVlJQVW1OSlFWSTRNRmd3VFVGWFFXTkdZelp2UjFKTVl5SXNJbmtpT2lKaU9GVk9ZMGhETW1GSFEzSjFTVFowUWxSV1NWWTBkVzVaV0VWeVMwTTRaRFJuUlRGR1owczBRMDVKSW4wIzAiLCJuYW1lIjoiQ29sb3JhZG8gV29ya2ZvcmNlIERldmVsb3BtZW50IENvdW5jaWwiLCJ0eXBlIjoiUHJvZmlsZSJ9LCJuYW1lIjoiQ29sb3JhZG9GV0RUZWFtTWVtYmVyc2hpcCIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJPcGVuQmFkZ2VDcmVkZW50aWFsIl19fQ.Aj40fUgmPygWVULtPgVWbpIsgbZjBp-KE_ZudRYpZ90PPGSbRf3hLEHZ9nkCKhYX7xCmTOtdYHnjhEwIFN1Xng~"; + +const val exampleSdJwt = + "eyJhbGciOiJFZERTQSJ9.eyJfc2RfYWxnIjoic2hhLTI1NiIsIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy9ucy9jcmVkZW50aWFscy92MiIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9jb250ZXh0LTMuMC4zLmpzb24iXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIk9wZW5CYWRnZUNyZWRlbnRpYWwiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWRlbnRpdHkiOlt7Ii4uLiI6ImxoeTloT3p6eWZZVFc0WWlockc4VVZRcHhFcWN1Nk9icFdvOVBhNExFRzAifSx7Imhhc2hlZCI6ZmFsc2UsImlkZW50aXR5SGFzaCI6ImpvaG4uc21pdGhAZXhhbXBsZS5jb20iLCJpZGVudGl0eVR5cGUiOiJlbWFpbEFkZHJlc3MiLCJzYWx0Ijoibm90LXVzZWQiLCJ0eXBlIjoiSWRlbnRpdHlPYmplY3QifV0sImFjaGlldmVtZW50Ijp7Im5hbWUiOiJUZWFtIE1lbWJlcnNoaXAiLCJ0eXBlIjoiQWNoaWV2ZW1lbnQifX0sImlzc3VlciI6eyJpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGVXpJMU5pSXNJbU55ZGlJNklsQXRNalUySWl3aWEzUjVJam9pUlVNaUxDSjRJam9pYldKVU0yZHFPV0Z2T0dOdVMyODBNMHByY1ZSUFVtTkpRVkk0TUZnd1RVRlhRV05HWXpadlIxSk1ZeUlzSW5raU9pSmlPRlZPWTBoRE1tRkhRM0oxU1RaMFFsUldTVlkwZFc1WldFVnlTME00WkRSblJURkdaMHMwUTA1SkluMCMwIiwibmFtZSI6Ildvcmtmb3JjZSBEZXZlbG9wbWVudCBDb3VuY2lsIiwidHlwZSI6IlByb2ZpbGUifSwiYXdhcmRlZERhdGUiOiIyMDI0LTA5LTIzVDE4OjEyOjEyKzAwMDAiLCJuYW1lIjoiVGVhbU1lbWJlcnNoaXAifQ.0zGx7-Fkio8bS4EZW_odUu3F6FG7nvRVEbGKIte2OZPXGM12XEhTJptty-1ZUgPAtP_jiyU_KTP3hPrGKKNnBg~WyJoS25GV0g0R3J5dEdVMkFLUFNJSDdRIix7Imhhc2hlZCI6ZmFsc2UsImlkZW50aXR5SGFzaCI6IkpvaG4gU21pdGgiLCJpZGVudGl0eVR5cGUiOiJuYW1lIiwic2FsdCI6Im5vdC11c2VkIiwidHlwZSI6IklkZW50aXR5T2JqZWN0In1d~" + +val trustedDids = MutableList(1) { "did:web:1741-24-113-196-42.ngrok-free.app:oid4vp:client" } diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/AchievementCredentialItem.kt b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/AchievementCredentialItem.kt index a93f2c9..40fd9be 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/AchievementCredentialItem.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/AchievementCredentialItem.kt @@ -66,6 +66,7 @@ class AchievementCredentialItem { constructor(rawCredential: String, onDelete: (() -> Unit)? = null) { val decodedSdJwt = decodeRevealSdJwt(rawCredential) + this.credential = JSONObject(decodedSdJwt) this.onDelete = onDelete } @@ -244,7 +245,10 @@ class AchievementCredentialItem { @Composable fun detailsComponent() { + println("credential: $credential") + // NOTE: The credential contains a `vc` property with the verifiable credential payload. val awardedDate = keyPathFinder(credential, mutableListOf("awardedDate")).toString() + println("awardedDate: $awardedDate") val ISO8601DateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss[.SSS]Z") val parsedDate = OffsetDateTime.parse(awardedDate, ISO8601DateFormat) val dateTimeFormatter = DateTimeFormatter.ofPattern("MMM dd, yyyy 'at' h:mm a") 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 9c35ec2..c712319 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/DispatchQRView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/DispatchQRView.kt @@ -2,39 +2,43 @@ 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.setValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.navigation.NavController +import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.spruceid.mobilesdkexample.ScanningComponent import com.spruceid.mobilesdkexample.ScanningType -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.spruceid.mobile.sdk.rs.verifyJwtVp -import kotlinx.coroutines.GlobalScope +import java.net.URLEncoder +import java.nio.charset.StandardCharsets import kotlinx.coroutines.launch +// The scheme for the OID4VP QR code. +const val OPEN_ID4VP_SCHEME = "openid4vp://" + @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) @Composable fun DispatchQRView( - navController: NavController + navController: NavController, ) { - var success by remember { - mutableStateOf(null) - } + val scope = rememberCoroutineScope() fun onRead(url: String) { - GlobalScope.launch { - // TODO: Add other checks as necessary for validating OID4VP url - // and handle OID4VP flow - - // dispatchQRcode(url) + println("Reading URL: $url") + + scope.launch { + if (url.contains(OPEN_ID4VP_SCHEME)) { + val encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8.toString()) + + navController.navigate("oid4vp/$encodedUrl") { + launchSingleTop = true + restoreState = true + } + } } } - + ScanningComponent( - navController = navController, - scanningType = ScanningType.QRCODE, - onRead = ::onRead + navController = navController, + scanningType = ScanningType.QRCODE, + onRead = ::onRead ) -} \ No newline at end of file +} diff --git a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VPView.kt b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VPView.kt new file mode 100644 index 0000000..0ba62d8 --- /dev/null +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/HandleOID4VPView.kt @@ -0,0 +1,201 @@ +package com.spruceid.mobilesdkexample.wallet + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.material3.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +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.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.google.accompanist.permissions.ExperimentalPermissionsApi +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.SdJwt +import com.spruceid.mobilesdkexample.ui.theme.Inter +import com.spruceid.mobilesdkexample.ui.theme.MobileSdkTheme +import com.spruceid.mobilesdkexample.viewmodels.IRawCredentialsViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Button +import com.spruceid.mobilesdkexample.navigation.Screen +import com.spruceid.mobilesdkexample.utils.trustedDids + + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) +@Composable +fun HandleOID4VPView( + navController: NavController, + rawCredentialsViewModel: IRawCredentialsViewModel, + url: String +) { + val scope = rememberCoroutineScope() + + val rawCredentials by rawCredentialsViewModel.rawCredentials.collectAsState() + + var holder by remember { mutableStateOf(null) } + var permissionRequest by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + println("URL: $url") + + try { + val credentials = rawCredentials.map { rawCredential -> + ParsedCredential + // TODO: Update to use VDC collection in the future + // to detect the type of credential. + .newSdJwt(SdJwt.newFromCompactSdJwt(rawCredential.rawCredential)) + .intoGenericForm() + } + + withContext(Dispatchers.IO) { + holder = Holder.newWithCredentials(credentials, trustedDids); + permissionRequest = holder!!.authorizationRequest(url) + } + } catch (e: Exception) { + println("Error: $e") + } + } + + if (permissionRequest == null) { + // Show a loading screen + MobileSdkTheme { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp) + ) { + Text( + text = "Loading... $url", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + ) + } + } + } else { + // Load the Credential View + CredentialSelector(navController, permissionRequest!!.credentials(), onSelectedCredential = { selectedCredentials -> + scope.launch { + try { + val selectedCredential = selectedCredentials.first() + val permissionResponse = permissionRequest!!.createPermissionResponse(selectedCredential) + + println("Submitting permission response") + + holder!!.submitPermissionResponse(permissionResponse) + } catch (e: Exception) { + println("Error: $e") + } + } + }) + } + +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CredentialSelector( + navController: NavController, + credentials: List, + onSelectedCredential: (List) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + val selectedCredentials = remember { mutableStateListOf() } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(48.dp) + ) { +// Text( +// text = "SpruceiD Demo Wallet", +// style = MaterialTheme.typography.headlineSmall +// ) +// +// Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Select the credential(s) to share", + style = MaterialTheme.typography.bodyLarge + ) + + Spacer(modifier = Modifier.height(8.dp)) + + ElevatedButton( + onClick = { expanded = !expanded }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = if (expanded) "Select All" else "Select Credentials") + } + + if (expanded) { + credentials.forEach { credential -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = credential in selectedCredentials, + onCheckedChange = { isChecked -> + if (isChecked) { + selectedCredentials.add(credential) + } else { + selectedCredentials.remove(credential) + } + } + ) + Text( + text = credential.format().toString(), + modifier = Modifier.padding(start = 8.dp) + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + OutlinedButton(onClick = { + // navigate back to home screen + navController.navigate(Screen.HomeScreen.route) + }) { + Text("Cancel") + } + Button(onClick = { + onSelectedCredential(selectedCredentials) + + // Navigate + navController.navigate(Screen.HomeScreen.route) + }) { + Text("Continue") + } + } + } +} \ 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 118d430..fde478b 100644 --- a/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt +++ b/example/src/main/java/com/spruceid/mobilesdkexample/wallet/WalletHomeView.kt @@ -12,21 +12,21 @@ 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.material3.Button -import androidx.compose.material3.ButtonDefaults 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.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.graphics.Color 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.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -37,25 +37,21 @@ import com.spruceid.mobilesdkexample.R import com.spruceid.mobilesdkexample.navigation.Screen import com.spruceid.mobilesdkexample.ui.theme.CTAButtonBlue import com.spruceid.mobilesdkexample.ui.theme.Inter -import com.spruceid.mobilesdkexample.ui.theme.TextHeader import com.spruceid.mobilesdkexample.ui.theme.Primary +import com.spruceid.mobilesdkexample.ui.theme.TextHeader import com.spruceid.mobilesdkexample.viewmodels.IRawCredentialsViewModel import kotlinx.coroutines.launch @Composable fun WalletHomeView( - navController: NavController, - rawCredentialsViewModel: IRawCredentialsViewModel + navController: NavController, + rawCredentialsViewModel: IRawCredentialsViewModel ) { - Column( - Modifier - .padding(all = 20.dp) - .padding(top = 20.dp) - ) { + Column(Modifier.padding(all = 20.dp).padding(top = 20.dp)) { WalletHomeHeader(navController = navController) WalletHomeBody( - rawCredentialsViewModel = rawCredentialsViewModel, - navController = navController + rawCredentialsViewModel = rawCredentialsViewModel, + navController = navController ) } } @@ -64,51 +60,45 @@ fun WalletHomeView( fun WalletHomeHeader(navController: NavController) { Row(verticalAlignment = Alignment.CenterVertically) { Text( - text = "SpruceKit Demo Wallet", - fontFamily = Inter, - fontWeight = FontWeight.SemiBold, - fontSize = 20.sp, - color = TextHeader + text = "SpruceKit Demo Wallet", + fontFamily = Inter, + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + color = TextHeader ) Spacer(Modifier.weight(1f)) Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .width(36.dp) - .height(36.dp) - .padding(start = 4.dp) - .clip(shape = RoundedCornerShape(8.dp)) - .background(Primary) - .clickable { - navController.navigate(Screen.OID4VCIScreen.route) - } + contentAlignment = Alignment.Center, + modifier = + Modifier.width(36.dp) + .height(36.dp) + .padding(start = 4.dp) + .clip(shape = RoundedCornerShape(8.dp)) + .background(Primary) + .clickable { navController.navigate(Screen.OID4VCIScreen.route) } ) { Image( - painter = painterResource(id = R.drawable.scan_qr_code), - contentDescription = stringResource(id = R.string.scan_qr_code), - modifier = Modifier - .width(20.dp) - .height(20.dp) + painter = painterResource(id = R.drawable.scan_qr_code), + contentDescription = stringResource(id = R.string.scan_qr_code), + modifier = Modifier.width(20.dp).height(20.dp) ) } Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .width(36.dp) - .height(36.dp) - .padding(start = 4.dp) - .clip(shape = RoundedCornerShape(8.dp)) - .background(Primary) - .clickable { - navController.navigate(Screen.WalletSettingsHomeScreen.route) - } + contentAlignment = Alignment.Center, + modifier = + Modifier.width(36.dp) + .height(36.dp) + .padding(start = 4.dp) + .clip(shape = RoundedCornerShape(8.dp)) + .background(Primary) + .clickable { + navController.navigate(Screen.WalletSettingsHomeScreen.route) + } ) { Image( - painter = painterResource(id = R.drawable.user), - contentDescription = stringResource(id = R.string.user), - modifier = Modifier - .width(20.dp) - .height(20.dp) + painter = painterResource(id = R.drawable.user), + contentDescription = stringResource(id = R.string.user), + modifier = Modifier.width(20.dp).height(20.dp) ) } } @@ -116,68 +106,62 @@ fun WalletHomeHeader(navController: NavController) { @Composable fun WalletHomeBody( - rawCredentialsViewModel: IRawCredentialsViewModel, - navController: NavController + rawCredentialsViewModel: IRawCredentialsViewModel, + navController: NavController ) { val scope = rememberCoroutineScope() val rawCredentials by rawCredentialsViewModel.rawCredentials.collectAsState() - if(rawCredentials.isNotEmpty()) { + if (rawCredentials.isNotEmpty()) { Box(modifier = Modifier.fillMaxSize()) { - LazyColumn( - Modifier - .fillMaxWidth() - .padding(top = 20.dp) - .padding(bottom = 60.dp) - ) { + LazyColumn(Modifier.fillMaxWidth().padding(top = 20.dp).padding(bottom = 60.dp)) { items(rawCredentials) { rawCredential -> AchievementCredentialItem( - rawCredential.rawCredential, - onDelete = { - scope.launch { - rawCredentialsViewModel.deleteRawCredential(id = rawCredential.id) - } - } - ).component() + rawCredential.rawCredential, + onDelete = { + scope.launch { + rawCredentialsViewModel.deleteRawCredential( + id = rawCredential.id + ) + } + } + ) + .component() } - // item { - // vcs.map { vc -> - // GenericCredentialListItems(vc = vc) - // } - // ShareableCredentialListItems(mdocBase64 = mdocBase64) - // } + // item { + // vcs.map { vc -> + // GenericCredentialListItems(vc = vc) + // } + // ShareableCredentialListItems(mdocBase64 = mdocBase64) + // } } - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.BottomCenter - ) { + 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, - ) + 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) + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(8.dp) ) { Icon( - painter = painterResource(id = R.drawable.scan_qr_code_white), - contentDescription = "QR Code Icon", - tint = Color.White, - modifier = Modifier.padding(end = 10.dp) + painter = painterResource(id = R.drawable.scan_qr_code_white), + contentDescription = "QR Code Icon", + tint = Color.White, + modifier = Modifier.padding(end = 10.dp) ) Text( - text = "Scan to share", - fontFamily = Inter, - fontWeight = FontWeight.Normal, - fontSize = 15.sp, + text = "Scan to share", + fontFamily = Inter, + fontWeight = FontWeight.Normal, + fontSize = 15.sp, ) } } @@ -187,9 +171,9 @@ fun WalletHomeBody( Column { Spacer(modifier = Modifier.weight(1f)) Image( - painter = painterResource(id = R.drawable.empty_wallet), - contentDescription = stringResource(id = R.string.user), - modifier = Modifier.weight(1f) + painter = painterResource(id = R.drawable.empty_wallet), + contentDescription = stringResource(id = R.string.user), + modifier = Modifier.weight(1f) ) Spacer(modifier = Modifier.weight(1f)) }