sample-application (#2135)

* sample-application

Signed-off-by: Kaushik Gupta <kausgpt97@gmail.com>

* refactor: Apply CodeRabbit review suggestions

Signed-off-by: Kaushik Gupta <kausgpt97@gmail.com>

* changes as per review

Signed-off-by: Kaushik Gupta <kausgpt97@gmail.com>

* fix: Add URL encoding for token request form parameters

Signed-off-by: Kaushik Gupta <kausgpt97@gmail.com>

* Remove sensitive JWT logging from proof generation

Signed-off-by: Kaushik Gupta <kausgpt97@gmail.com>

---------

Signed-off-by: Kaushik Gupta <kausgpt97@gmail.com>
This commit is contained in:
Kaushik Gupta
2025-11-18 09:38:25 +05:30
committed by GitHub
parent 9898c2abfc
commit f1a466cef7
64 changed files with 3951 additions and 0 deletions

View File

@@ -417,3 +417,8 @@ fileignoreconfig:
checksum: 4d46111af02fc8b6731f624d3e4f6ce71d6369649f4549564627a8962b136192
- filename: components/ui/Picker.tsx
checksum: ad814e904cb990f55068281fcb566cca349872de2c4fd66432ed6372fb540ec3
- filename: sample-application/sample-application/app/src/main/java/com/example/samplecredentialwallet/utils/SecureKeyStoreManager.kt
checksum: d9c999da50bf114d2bdb73550f94ad77003f252d97dd5c2cd2f0f14c2f4004a0
- filename: sample-application/sample-application/app/src/main/java/com/example/samplecredentialwallet/utils/CredentialParser.kt
checksum: bb910d0b02d8623c9f259a4e2a335dd8f9289c1949955538e65bab9179b09359
version: "1.0"

View File

@@ -0,0 +1,237 @@
# Sample Credential Wallet
## Introduction
This README provides a step-by-step [guide](https://docs.inji.io/inji-wallet/inji-mobile/technical-overview/integration-guide/building-verifiable-credentials-wallet-with-inji-libraries#inji-libraries-used-for-building-a-custom-wallet) for developers to build their own Verifiable Credential (VC) wallet using the Inji [libraries](https://docs.inji.io/inji-wallet/inji-mobile/technical-overview/integration-guide). By leveraging Injis modular SDKs, developers can easily integrate core VC wallet capabilities such as downloading, storing, sharing, and verifying credentials. The implementation examples are based on Kotlin for Android, but the same libraries are also available in Swift for iOS, enabling developers to create secure, interoperable, and cross-platform wallet applications powered by Inji. The **Sample Verifiable Credential Wallet** is a **Kotlin-based Android application** that demonstrates the **end-to-end Verifiable Credential (VC) lifecycle** within the **Inji ecosystem** — covering:
- **Credential issuance** via [Inji Certify](https://docs.inji.io/inji-certify/overview)
- **Secure storage and management** through [Inji Libraries/SDKs](https://docs.inji.io/inji-wallet/inji-mobile/technical-overview/integration-guide)
- **Verification** using [Inji Verify](https://docs.inji.io/inji-verify/overview)
It serves as a reference implementation for developers integrating Inji Mobile Wallet library components to build secure and standards-compliant credential wallet applications.
### Purpose
The purpose of the Sample Credential Wallet is to help developers understand and implement the verifiable credential lifecycle using the Inji ecosystem. It demonstrates:
- Implementation of OpenID4VCI protocol for credential requests using Inji Library components
- Hardware-backed key generation and JWT signing for proof generation
- Full credential lifecycle — issuance → verification → storage
### Key Components
| Component | Description |
| --------------- | ------------------------------------------------------------------------------------- |
| Inji Certify | Credential issuer implementing the OpenID4VCI protocol for secure issuance |
| VCI Client | OpenID4VCI client library handling authorization and credential download flows |
| Secure Keystore | Android hardware-backed Keystore manager for RSA/EC key pair generation |
| VC Verifier | Verification library validating credential signatures and structure using vc verifier |
To get more details, click [here](https://docs.inji.io/inji-wallet/inji-mobile/technical-overview/integration-guide/building-verifiable-credentials-wallet-with-inji-libraries#inji-libraries-used-for-building-a-custom-wallet) !
**Integrated Libraries:**
- `inji-vci-client:0.5.0`
- `secure-keystore:0.3.0`
- `vcverifier-aar:1.4.0`
### Key Features
- **Keypair Generation** RSA (RS256) and EC (ES256) key generation
- **JWT Proof Signing** Proof JWT generation for OpenID4VCI
- **Credential Issuance** Authorization Code Flow-based issuance
- **Credential Verification** Signature validation via vc verifier
To get more details, [click here](https://docs.inji.io/inji-wallet/inji-mobile/technical-overview/integration-guide/building-verifiable-credentials-wallet-with-inji-libraries#inji-libraries-used-for-building-a-custom-wallet) !
## Pre-requisites
### Development Environment
- **Android Studio** Jellyfish (2023.3.1) or later
- **JDK** 11 or higher
- **Gradle** 8.9.0 or higher
- **Android SDK**
- Min API: 26
- Target API: 35
- Compile SDK: 35
### Dependencies
Managed via Gradle (no Docker/Postgres setup required).
## Installation & Setup
### 1. Clone Repository
```bash
git clone https://github.com/mosip/inji-wallet.git
cd inji-wallet
```
### 2. Open in Android Studio
- Launch Android Studio
- Go to **File → Open**
- Select `sample-application/sample-application` directory
- Wait for Gradle sync to complete
### 3. Verify Dependencies
```kotlin
dependencies {
implementation("io.mosip:inji-vci-client-aar:0.5.0")
implementation("io.mosip:secure-keystore:0.3.0")
implementation("io.mosip:vcverifier-aar:1.4.0")
implementation("com.nimbusds:nimbus-jose-jwt:10.6")
}
```
Update versions if needed:
```bash
./gradlew build --refresh-dependencies
```
## Configuration
Configuration resides in `navigation/AppNavHost.kt`:
```kotlin
object Constants {
var credentialIssuerHost = "https://injicertify-mosipid.collab.mosip.net"
var credentialTypeId = "MosipVerifiableCredential"
var clientId = "mpartner-default-mimoto-mosipid-oidc"
var redirectUri = "io.mosip.residentapp.inji://oauthredirect"
}
```
**Available Issuers:**
- Mosip National ID
- Insurance Credential
- Tax Account Credential
- Land Registry Credential
## Build & Run
### Build and Install (Debug Mode)
```bash
adb devices
./gradlew installDebug
```
### Generate APK
```bash
./gradlew assembleDebug
adb install app/build/outputs/apk/debug/app-debug.apk
```
## How to Use
### Step 1. App Launch → Keystore Initialization
Automatically generates hardware-backed RSA/EC key pairs.
### Step 2. Select Issuer
Choose a credential issuer (e.g., Veridonia National ID or Insurance).
### Step 3. Request Credential (OpenID4VCI Flow)
Performs Authorization Code Flow → Proof JWT generation → Credential download.
### Step 4. Verify Credential
Credential is validated via vc verifier library.
### Step 5. View Stored Credentials
Downloaded credentials are listed in-app and can be verified offline.
## API Usage Examples
### SecureKeystoreManager
```kotlin
val keystoreManager = SecureKeystoreManager.getInstance(context)
keystoreManager.initializeKeystore()
val publicKey = keystoreManager.getPublicKey(KeyType.RS256).getOrNull()
```
### VCIClient Integration
```kotlin
val client = VCIClient("instance-id")
val metadata = ClientMetadata(clientId, redirectUri)
val authUrl = client.getAuthorizationUrl(issuerHost, metadata, credentialType)
```
### CredentialStore
```kotlin
CredentialStore.addCredential(credentialJson)
val credentials = CredentialStore.getAllCredentials()
```
## Project Structure
```
app/src/main/java/com/example/samplecredentialwallet/
├── MainActivity.kt # Entry point, keystore initialization
├── navigation/
│ └── AppNavHost.kt # Navigation & issuer configuration
├── screens/
│ ├── HomeScreen.kt # Issuer selection
│ ├── CredentialDownloadScreen.kt # OpenID4VCI flow
│ ├── CredentialListScreen.kt # Credential list
│ └── CredentialDetailScreen.kt # Credential details
└── utils/
├── SecureKeystoreManager.kt # Android Keystore wrapper
├── CredentialVerifier.kt # vc verifier integration
└── CredentialStore.kt # In-memory storage
```
## Error Handling
| Error | Cause | Solution |
| ------------------------------ | ------------------------ | ------------------------------ |
| Keystore Initialization Failed | Hardware not supported | Use API 26+ physical device |
| No Internet Connection | Network issues | Verify connectivity |
| Authorization Failed | Invalid redirect or code | Check redirect URI |
| Credential Download Failed | Token or nonce error | Regenerate proof JWT |
| Verification Skipped | Incompatible verifier | JSON structure validation used |
| Invalid JSON Structure | Malformed credential | Validate issuer response |
**Debug Commands:**
```bash
./gradlew clean
adb logcat -c && adb logcat MainActivity:D AuthCodeHolder:D CredentialDownload:D AppNavHost:D PROOF_JWT:D PROOF_JWT_CLAIMS:D PROOF_JWT_FINAL:D *:S
```
## Demo Reference
A demo video will be `available soon`, providing a walkthrough of the following features:
- Keystore initialization
- Issuer selection and credential request
- OpenID4VCI authorization flow
- JWT proof generation
- Credential download, verification, and storage
## Additional Resources
- [Inji Wallet GitHub Repository](https://github.com/mosip/inji-wallet)
- [Inji VCI Client Repository](https://github.com/mosip/inji-vci-client)
- [Secure Keystore Repository](https://github.com/mosip/secure-keystore/tree/master)
- [VC Verifier Repository](https://github.com/mosip/vc-verifier)
- [OpenID4VCI Specification Draft 13](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-ID1.html)
- [Building Verifiable Credentials Wallet with Inji Libraries](https://docs.inji.io/inji-wallet/inji-mobile/technical-overview/integration-guide/building-verifiable-credentials-wallet-with-inji-libraries)
- [OpenID4VP Specification Draft 23](https://openid.net/specs/openid-4-verifiable-presentations-1_0-ID3.html)
## License
This project is licensed under the [MOSIP Open License](https://docs.mosip.io/1.2.0/readme/license).

View File

@@ -0,0 +1,14 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx

View File

@@ -0,0 +1,4 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready

View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,127 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.example.samplecredentialwallet"
compileSdk = 35
defaultConfig {
applicationId = "com.example.samplecredentialwallet"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
packaging {
resources {
excludes += setOf(
"META-INF/license.txt",
"META-INF/LICENSE.txt",
"META-INF/LICENSE",
"META-INF/notice.txt",
"META-INF/NOTICE.txt",
"META-INF/NOTICE",
"META-INF/DEPENDENCIES",
"META-INF/ASL2.0",
"META-INF/*.kotlin_module"
)
}
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation("io.mosip:inji-vci-client-aar:0.5.0") {
// Exclude transitive dependencies to use explicitly declared versions
exclude(group = "com.apicatalog", module = "titanium-json-ld-jre8")
exclude(group = "org.bouncycastle")
}
implementation("com.nimbusds:nimbus-jose-jwt:10.6") //JWT Signing Library
implementation("io.mosip:secure-keystore:0.3.0") { // Secure Keystore Library
// Exclude transitive dependencies to prevent conflicts and use explicitly declared versions
exclude(group = "org.bouncycastle")
exclude(group = "org.springframework")
exclude(group = "com.apicatalog", module = "titanium-json-ld-jre8")
}
implementation("io.mosip:vcverifier-aar:1.4.0") { // Verifiable Credential Verification Library
// Exclude transitive dependencies to prevent conflicts and use explicitly declared versions
exclude(group = "org.bouncycastle")
exclude(group = "org.springframework")
exclude(group = "com.apicatalog", module = "titanium-json-ld-jre8")
}
implementation("androidx.navigation:navigation-compose:2.8.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.5")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.5")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.browser:browser:1.8.0")
implementation("androidx.biometric:biometric:1.1.0")
implementation("androidx.fragment:fragment-ktx:1.8.5")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
implementation("io.mosip:pixelpass-aar:0.7.0") {
// Exclude transitive dependencies to prevent conflicts and use explicitly declared versions
exclude(group = "org.bouncycastle")
exclude(group = "org.springframework")
exclude(group = "com.apicatalog", module = "titanium-json-ld-jre8")
}
implementation("com.apicatalog:titanium-json-ld:1.3.2")
implementation("org.bouncycastle:bcprov-jdk18on:1.74")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}

View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,24 @@
package com.example.samplecredentialwallet
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.samplecredentialwallet", appContext.packageName)
}
}

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Existing permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Required permissions for biometrics and keystore -->
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<!-- Feature declarations -->
<uses-feature android:name="android.hardware.fingerprint" android:required="false" />
<uses-feature android:name="android.hardware.biometric" android:required="false" />
<application
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SampleCredentialWallet"
tools:targetApi="31">
<activity
android:name="com.example.samplecredentialwallet.MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.SampleCredentialWallet"
android:launchMode="singleTop">
<!-- Launcher -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Deep link for OAuth redirect -->
<intent-filter android:label="authRedirect">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Matches redirect URI: io.mosip.residentapp.inji://oauthredirect -->
<data
android:scheme="io.mosip.residentapp.inji"
android:host="oauthredirect" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,100 @@
package com.example.samplecredentialwallet
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.rememberNavController
import com.example.samplecredentialwallet.navigation.AppNavHost
import com.example.samplecredentialwallet.utils.SecureKeystoreManager
import com.example.samplecredentialwallet.utils.AuthCodeHolder
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
companion object {
private const val TAG = "MainActivity"
}
private lateinit var keystoreManager: SecureKeystoreManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Initialize keystore manager
keystoreManager = SecureKeystoreManager.getInstance(this)
// Check and initialize keystore
initializeKeystoreIfNeeded()
setContent {
val navController = rememberNavController()
// Set up your NavHost
AppNavHost(navController = navController)
// Handle deeplink when activity is created
LaunchedEffect(Unit) {
handleDeeplink(intent)
}
}
}
private fun initializeKeystoreIfNeeded() {
// Check if keys are already generated
if (keystoreManager.areKeysGenerated()) {
Log.i(TAG, "Keys already exist, skipping generation")
return
}
// Generate keys for first time
lifecycleScope.launch {
try {
val result = keystoreManager.initializeKeystore()
if (result.isSuccess) {
val message = result.getOrNull() ?: "Key pairs generated successfully!"
Log.i(TAG, message)
// Log keystore status
val status = keystoreManager.getKeystoreStatus()
Log.d(TAG, "Keystore Status: $status")
} else {
val error = result.exceptionOrNull()
Log.e(TAG, "Keystore initialization failed", error)
}
} catch (e: Exception) {
Log.e(TAG, "Error during keystore initialization", e)
}
}
}
private fun showToast(message: String) {
runOnUiThread {
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleDeeplink(intent)
}
private fun handleDeeplink(intent: Intent?) {
intent?.data?.let { uri: Uri ->
if (uri.toString().startsWith("io.mosip.residentapp.inji://oauthredirect")) {
val code = uri.getQueryParameter("code")
Log.d(TAG, "⚡ handleDeeplink triggered with code=$code")
AuthCodeHolder.complete(code)
}
}
}
}

View File

@@ -0,0 +1,103 @@
package com.example.samplecredentialwallet.navigation
import android.net.Uri
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.example.samplecredentialwallet.ui.credential.CredentialDownloadScreen
import com.example.samplecredentialwallet.ui.credential.CredentialListScreen
import com.example.samplecredentialwallet.ui.home.HomeScreen
import com.example.samplecredentialwallet.ui.issuer.IssuerListScreen
import com.example.samplecredentialwallet.ui.issuer.IssuerDetailScreen
import com.example.samplecredentialwallet.ui.auth.AuthWebViewScreen
import com.example.samplecredentialwallet.ui.splash.SplashScreen
import com.example.samplecredentialwallet.utils.Constants
import com.example.samplecredentialwallet.utils.IssuerRepository
sealed class Screen(val route: String) {
object Splash : Screen("splash")
object Home : Screen("home")
object IssuerList : Screen("issuer_list")
object IssuerDetail : Screen("issuer_detail/{issuerType}") {
fun createRoute(issuerType: String) = "issuer_detail/$issuerType"
}
object CredentialDetail : Screen("credential_detail?authCode={authCode}") {
fun createRoute(authCode: String?) = "credential_detail?authCode=$authCode"
}
object CredentialList : Screen("credential_list?index={index}") {
fun createRoute(index: Int = -1) = "credential_list?index=$index"
}
object AuthWebView : Screen("auth_webview/{authUrl}") {
fun createRoute(authUrl: String): String {
// Encode the URL before putting it into the route
return "auth_webview/${Uri.encode(authUrl)}"
}
}
}
@Composable
fun AppNavHost(navController: NavHostController) {
NavHost(navController = navController, startDestination = Screen.Splash.route) {
composable(Screen.Splash.route) {
SplashScreen {
navController.navigate(Screen.Home.route) {
popUpTo(Screen.Splash.route) { inclusive = true }
}
}
}
composable(Screen.Home.route) {
HomeScreen(
onNavigate = { navController.navigate(Screen.IssuerList.route) },
onViewCredential = { index ->
navController.navigate(Screen.CredentialList.createRoute(index))
}
)
}
composable(Screen.IssuerList.route) {
IssuerListScreen(
onIssuerClick = { issuerType ->
// Apply issuer configuration from repository
if (IssuerRepository.applyConfiguration(issuerType)) {
// Navigate to credential download
navController.navigate(Screen.CredentialDetail.route)
} else {
Log.e("AppNavHost", "Unknown issuer type: $issuerType")
}
}
)
}
composable(Screen.AuthWebView.route) { backStackEntry ->
val encodedUrl = backStackEntry.arguments?.getString("authUrl") ?: ""
val authUrl = Uri.decode(encodedUrl) // decode back
AuthWebViewScreen(
authorizationUrl = authUrl,
redirectUri = Constants.redirectUri ?: "",
navController = navController
)
}
composable(
route = Screen.CredentialDetail.route,
arguments = listOf(navArgument("authCode") { nullable = true })
) { backStackEntry ->
val authCode = backStackEntry.arguments?.getString("authCode")
CredentialDownloadScreen(navController, authCode)
}
composable(
route = Screen.CredentialList.route,
arguments = listOf(navArgument("index") {
type = androidx.navigation.NavType.IntType
defaultValue = -1
})
) { backStackEntry ->
val credentialIndex = backStackEntry.arguments?.getInt("index") ?: -1
Log.d("AppNavHost", "Navigating to credential list with index: $credentialIndex")
CredentialListScreen(navController, credentialIndex)
}
}
}

View File

@@ -0,0 +1,227 @@
package com.example.samplecredentialwallet.ui.auth
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.net.Uri
import android.util.Log
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.navigation.NavController
import com.example.samplecredentialwallet.navigation.Screen
import com.example.samplecredentialwallet.utils.AuthCodeHolder
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun AuthWebViewScreen(
authorizationUrl: String,
redirectUri: String,
navController: NavController
) {
var isLoading by remember { mutableStateOf(true) }
var isDownloading by remember { mutableStateOf(false) }
var currentUrl by remember { mutableStateOf("") }
var errorMessage by remember { mutableStateOf<String?>(null) }
Box(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier.fillMaxSize()
) {
// Header with loading indicator
if (isLoading) {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
color = androidx.compose.ui.graphics.Color(0xFFF2680C)
)
}
Text(
text = "Authenticating...",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
maxLines = 1
)
// WebView
AndroidView(
factory = { context ->
WebView(context).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.loadWithOverviewMode = true
settings.useWideViewPort = true
webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
Log.d("AuthWebView", "Page started: $url")
currentUrl = url ?: ""
isLoading = true
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
Log.d("AuthWebView", "Page finished: $url")
isLoading = false
}
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
Log.d("AuthWebView", "shouldOverrideUrlLoading: $url")
if (url != null && url.startsWith(redirectUri)) {
Log.d("AuthWebView", "Redirect URI matched: $url")
val uri = Uri.parse(url)
val code = uri.getQueryParameter("code")
val error = uri.getQueryParameter("error")
Log.d("AuthWebView", "Auth code present: ${code != null}, Error: $error")
if (code != null) {
Log.d("AuthWebView", "Completing auth flow with code")
AuthCodeHolder.complete(code)
isLoading = true
isDownloading = true
errorMessage = null
} else if (error != null) {
Log.e("AuthWebView", "Auth error: $error")
isLoading = false
isDownloading = false
errorMessage = "Authentication failed: $error"
AuthCodeHolder.complete(null)
} else {
Log.w("AuthWebView", "No code or error in redirect")
isLoading = false
isDownloading = false
errorMessage = "Authentication failed: No authorization code received"
AuthCodeHolder.complete(null)
}
// Don't navigate back - stay on this screen with loader
// The download will complete in background and navigate when done
return true
}
return false
}
override fun onReceivedError(
view: WebView?,
errorCode: Int,
description: String?,
failingUrl: String?
) {
super.onReceivedError(view, errorCode, description, failingUrl)
Log.e("AuthWebView", "WebView error: $errorCode - $description for $failingUrl")
isLoading = false
isDownloading = false
errorMessage = "Failed to load page: ${description ?: "Unknown error"}"
}
}
Log.d("AuthWebView", "Loading authorization URL: $authorizationUrl")
loadUrl(authorizationUrl)
}
},
modifier = Modifier.fillMaxSize()
)
}
// Loading overlay when page is loading or downloading
if (isLoading || isDownloading) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White.copy(alpha = 0.95f)),
contentAlignment = Alignment.Center
) {
Card(
colors = CardDefaults.cardColors(
containerColor = Color.White
),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator(
modifier = Modifier.size(56.dp),
color = Color(0xFFF2680C)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = if (isDownloading) "Downloading Credential..." else "Loading Authentication...",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Please wait...",
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
}
}
}
}
// Error overlay when there's an error
errorMessage?.let { message ->
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White.copy(alpha = 0.95f)),
contentAlignment = Alignment.Center
) {
Card(
colors = CardDefaults.cardColors(
containerColor = Color.White
),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Error",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = Color.Red
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = message,
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray
)
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = {
errorMessage = null
navController.popBackStack()
},
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFF2680C)
)
) {
Text("Go Back")
}
}
}
}
}
}
}

View File

@@ -0,0 +1,606 @@
package com.example.samplecredentialwallet.ui.credential
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.example.samplecredentialwallet.navigation.Screen
import com.example.samplecredentialwallet.utils.AuthCodeHolder
import com.example.samplecredentialwallet.utils.Constants
import com.example.samplecredentialwallet.utils.CredentialStore
import com.example.samplecredentialwallet.utils.CredentialVerifier
import com.example.samplecredentialwallet.utils.SecureKeystoreManager
import com.example.samplecredentialwallet.utils.EndpointConfig
import com.nimbusds.jose.JOSEObjectType
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.JWSHeader
import com.nimbusds.jose.crypto.RSASSASigner
import com.nimbusds.jose.crypto.ECDSASigner
import com.nimbusds.jose.jwk.RSAKey
import com.nimbusds.jose.jwk.ECKey
import com.nimbusds.jose.jwk.Curve
import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.jwt.SignedJWT
import io.mosip.vciclient.VCIClient
import io.mosip.vciclient.authorizationCodeFlow.clientMetadata.ClientMetadata
import io.mosip.vciclient.token.TokenRequest
import io.mosip.vciclient.token.TokenResponse
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withTimeout
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL
import java.net.URLEncoder
import java.net.UnknownHostException
import java.util.Base64
import java.util.Date
import java.security.KeyStore
import java.security.PrivateKey
import java.security.interfaces.RSAPublicKey
import java.security.interfaces.ECPrivateKey
import java.security.interfaces.ECPublicKey
@Composable
fun CredentialDownloadScreen(
navController: NavController,
authCode: String? = null
) {
val context = LocalContext.current
// Initialize and ensure keys exist (hardware-backed when available)
val keystoreManager = remember { SecureKeystoreManager.getInstance(context) }
LaunchedEffect(Unit) {
try {
keystoreManager.initializeKeystore()
} catch (e: Exception) {
Log.e("CredentialDownload", "Keystore initialization failed: ${e.message}", e)
}
}
val client = VCIClient("demo-123")
val clientMetadata = ClientMetadata(
clientId = Constants.clientId.toString(),
redirectUri = Constants.redirectUri.toString()
)
var tokenResponseJson by remember { mutableStateOf<String?>(null) }
val isLoading = remember { mutableStateOf(false) }
val loadingMessage = remember { mutableStateOf("Downloading Credential...") }
val errorMessage = remember { mutableStateOf<String?>(null) }
val showError = remember { mutableStateOf(false) }
Box(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.Top
) {
Text(
text = "Download Credential",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(16.dp))
Text(
"OpenID4VCI Flow",
style = MaterialTheme.typography.titleMedium,
color = Color.Gray
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"Credential Type: ${Constants.credentialDisplayName ?: Constants.credentialTypeId}",
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = {
GlobalScope.launch(Dispatchers.IO) {
try {
withContext(Dispatchers.Main) {
isLoading.value = true
loadingMessage.value = "Starting credential download..."
}
withTimeout(600000L) {
val credential = client.requestCredentialFromTrustedIssuer(
credentialIssuer = Constants.credentialIssuerHost.toString(),
credentialConfigurationId = Constants.credentialTypeId.toString(),
clientMetadata = clientMetadata,
authorizeUser = { url ->
Log.d("AUTH_FLOW", "Authorization flow started")
Log.d("AUTH_FLOW", "Authorization URL: $url")
withContext(Dispatchers.Main) {
loadingMessage.value = "Authenticating..."
}
val code = handleAuthorizationFlow(navController, url)
Log.d("AUTH_FLOW", "Authorization code received")
code
},
getTokenResponse = { tokenRequest ->
Log.d("TOKEN_EXCHANGE", "Token exchange started")
Log.d("TOKEN_EXCHANGE", "Token endpoint: ${tokenRequest.tokenEndpoint}")
withContext(Dispatchers.Main) {
loadingMessage.value = "Exchanging tokens..."
}
// Resolve token endpoint using configuration
val endpoint = EndpointConfig.resolveTokenEndpoint(
tokenRequest.tokenEndpoint,
Constants.credentialIssuerHost
)
Log.d("TOKEN_EXCHANGE", "Using custom endpoint: $endpoint")
val response = sendTokenRequest(tokenRequest, endpoint)
Log.d("TOKEN_EXCHANGE", "Access token received")
Log.d("TOKEN_EXCHANGE", "c_nonce received")
TokenResponse(
accessToken = response.getString("access_token"),
tokenType = response.getString("token_type"),
expiresIn = response.optInt("expires_in"),
cNonce = response.optString("c_nonce"),
cNonceExpiresIn = response.optInt("c_nonce_expires_in")
)
},
getProofJwt = { issuer, cNonce, _ ->
Log.d("PROOF_JWT", "Proof JWT generation started")
Log.d("PROOF_JWT", "Issuer: $issuer")
Log.d("PROOF_JWT", "c_nonce: $cNonce")
withContext(Dispatchers.Main) {
loadingMessage.value = "Generating proof..."
}
val proofJwt = signProofJWT(cNonce, issuer, isTrusted = true, context = context)
proofJwt
}
)
Log.d("VC_DOWNLOAD", "Credential download completed")
Log.d("VC_DOWNLOAD", "Credential object received: ${credential?.javaClass?.simpleName}")
withContext(Dispatchers.Main) {
loadingMessage.value = "Processing credential..."
if (credential == null) {
Log.e("VC_DOWNLOAD", "Credential is null")
isLoading.value = false
showError.value = true
errorMessage.value = "Something went wrong!"
return@withContext
}
credential.let { credObj ->
// Extract credential string from response object
val credentialStr = try {
Log.d("VC_EXTRACT", "Extracting credential from response object")
var credField: String? = null
try {
val method = credObj.javaClass.getMethod("getCredential")
credField = method.invoke(credObj) as? String
Log.d("VC_EXTRACT", "Method successful: getCredential()")
} catch (e: Exception) {
Log.d("VC_EXTRACT", "Method failed: ${e.message}")
}
if (credField == null) {
try {
val field = credObj.javaClass.getDeclaredField("credential")
field.isAccessible = true
credField = field.get(credObj) as? String
Log.d("VC_EXTRACT", "Method successful: field access")
} catch (e: Exception) {
Log.d("VC_EXTRACT", "Method failed: ${e.message}")
}
}
if (credField == null) {
Log.d("VC_EXTRACT", "Trying Method : regex parsing")
val str = credObj.toString()
val credentialMatch = Regex("""credential=(\{.*\})(?:,|\))""").find(str)
if (credentialMatch != null) {
credField = credentialMatch.groupValues[1]
Log.d("VC_EXTRACT", "Method successful: regex parsing")
}
}
credField ?: credObj.toString()
} catch (e: Exception) {
Log.e("VC_EXTRACT", "Failed to extract credential: ${e.message}")
e.printStackTrace()
credObj.toString()
}
Log.d("VC_EXTRACT", "Credential extracted successfully")
Log.d("VC_EXTRACT", "Credential length: ${credentialStr.length} characters")
tokenResponseJson = credentialStr
Log.d("VC_VERIFY", "Starting credential verification")
val verified = CredentialVerifier.verifyCredential(credentialStr, demoMode = true)
Log.d("VC_VERIFY", "Verification result: $verified")
// Add display name to credential before storing
val credentialWithDisplayName = try {
val credJson = org.json.JSONObject(credentialStr)
Constants.credentialDisplayName?.let { displayName ->
credJson.put("credentialName", displayName)
Log.d("VC_STORE", "Added display name: $displayName")
}
credJson.toString()
} catch (e: Exception) {
Log.e("VC_STORE", "Failed to add display name: ${e.message}")
credentialStr
}
// Store credential
Log.d("VC_STORE", "Storing credential in credential store")
CredentialStore.addCredential(credentialWithDisplayName)
Log.d("VC_STORE", "Credential stored successfully")
isLoading.value = false
// Navigate back to home screen
navController.navigate(Screen.Home.route) {
// Pop everything including auth_webview and credential_detail
popUpTo(Screen.Home.route) { inclusive = true }
}
}
}
}
} catch (e: Exception) {
Log.e("CredentialDownload", "Download failed: ${e.message}", e)
// CRITICAL: Must switch to Main dispatcher to update UI state
withContext(Dispatchers.Main) {
isLoading.value = false
showError.value = true
// Different error messages based on error type
errorMessage.value = when {
e is UnknownHostException -> "No internet connection"
e is java.net.SocketTimeoutException -> "No internet connection"
e is java.net.ConnectException -> "No internet connection"
e.message?.contains("Unable to resolve host", ignoreCase = true) == true -> "No internet connection"
e.message?.contains("timeout", ignoreCase = true) == true -> "No internet connection"
else -> "Something went wrong!"
}
Log.e("CredentialDownload", "Error UI shown: ${errorMessage.value}")
// Also navigate away from AuthWebView so user doesn't get stuck on its loader
try {
navController.navigate(Screen.Home.route) {
popUpTo(Screen.Home.route) { inclusive = true }
}
} catch (navE: Exception) {
Log.w("CredentialDownload", "Navigation after error failed: ${navE.message}")
}
}
} finally {
// Safety net: Must run on Main dispatcher
withContext(Dispatchers.Main) {
if (isLoading.value) {
isLoading.value = false
showError.value = true
errorMessage.value = "Something went wrong!"
Log.e("CredentialDownload", "Finally block: Error UI forced")
}
}
}
}
},
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading.value,
colors = ButtonDefaults.buttonColors(
containerColor = com.example.samplecredentialwallet.ui.theme.InjiOrange
)
) {
if (isLoading.value) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = Color.White
)
Spacer(modifier = Modifier.width(8.dp))
Text("Downloading...")
} else {
Text("Download Credential")
}
}
}
// Loading overlay
if (isLoading.value) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.5f)),
contentAlignment = Alignment.Center
) {
Card(
colors = CardDefaults.cardColors(
containerColor = Color.White
),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator(
modifier = Modifier.size(48.dp),
color = com.example.samplecredentialwallet.ui.theme.InjiOrange
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = loadingMessage.value,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Please wait...",
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
}
}
}
}
// Error Screen Overlay
if (showError.value && errorMessage.value != null) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
contentAlignment = Alignment.Center
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFFFF3E0) // Light orange background
),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Error Icon
Icon(
imageVector = Icons.Default.Info,
contentDescription = "Error",
tint = Color(0xFFF57C00), // Orange color
modifier = Modifier.size(72.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = errorMessage.value ?: "Something went wrong!",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = Color.Black
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = if (errorMessage.value == "No internet connection") {
"Please check your internet connection and try again."
} else {
"We are having some trouble with your request. Please try again."
},
style = MaterialTheme.typography.bodyLarge,
color = Color.Gray,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = {
showError.value = false
errorMessage.value = null
navController.navigate(Screen.Home.route) {
popUpTo(Screen.Home.route) { inclusive = true }
}
},
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = com.example.samplecredentialwallet.ui.theme.InjiOrange
)
) {
Text("Try again", fontSize = 16.sp)
}
}
}
}
}
}
}
suspend fun handleAuthorizationFlow(
navController: NavController,
url: String
): String {
withContext(Dispatchers.Main) {
navController.navigate(Screen.AuthWebView.createRoute(url))
}
val code = AuthCodeHolder.waitForCode()
return code
}
private fun signProofJWT(
cNonce: String?,
issuer: String,
isTrusted: Boolean,
context: android.content.Context
): String {
// Validate required dynamic inputs
val nonNullNonce = cNonce?.trim()?.takeIf { it.isNotEmpty() }
?: throw IllegalStateException("c_nonce missing from token response; cannot build proof JWT")
val clientId = Constants.clientId?.takeIf { it.isNotBlank() }
?: throw IllegalStateException("clientId not initialized in Constants; call the appropriate ViewModel setup before starting download")
val manager = SecureKeystoreManager.getInstance(context)
val useEc = manager.hasKey(SecureKeystoreManager.KeyType.ES256)
val useRsa = manager.hasKey(SecureKeystoreManager.KeyType.RS256)
if (!useEc && !useRsa) {
throw IllegalStateException("No keystore key available. Initialize keystore before signing.")
}
val (alg, publicJwk) = if (useRsa) {
JWSAlgorithm.RS256 to buildPublicRsaJwkFromAndroid(SecureKeystoreManager.KeyType.RS256.value)
} else {
JWSAlgorithm.ES256 to buildPublicEcJwkFromAndroid(SecureKeystoreManager.KeyType.ES256.value)
}
Log.d("PROOF_JWT", "Algorithm: $alg")
Log.d("PROOF_JWT", "Public key type: ${publicJwk.keyType}")
val header = JWSHeader.Builder(alg)
.type(JOSEObjectType("openid4vci-proof+jwt"))
.jwk(publicJwk)
.build()
Log.d("PROOF_JWT", "JWT Header created with type: openid4vci-proof+jwt")
val audience = (Constants.credentialIssuerHost ?: issuer)
val now = System.currentTimeMillis()
val claimsSet = JWTClaimsSet.Builder()
.issuer(clientId)
.audience(audience)
.claim("nonce", nonNullNonce)
.issueTime(Date(now))
.expirationTime(Date(now + 3 * 60 * 1000))
.build()
// Note: JWT claims contain sensitive data (nonce, etc.) - avoid logging in production
Log.d("PROOF_JWT", "Signing JWT with algorithm: $alg")
val signedJWT = SignedJWT(header, claimsSet).apply {
if (alg == JWSAlgorithm.RS256) {
val privateKey = loadPrivateKey(SecureKeystoreManager.KeyType.RS256.value)
sign(RSASSASigner(privateKey))
Log.d("PROOF_JWT", "Signed with RS256 private key")
} else {
val privateKey = loadPrivateKey(SecureKeystoreManager.KeyType.ES256.value) as ECPrivateKey
sign(ECDSASigner(privateKey))
Log.d("PROOF_JWT", "Signed with ES256 private key")
}
}
// Note: Serialized JWT contains sensitive proof - avoid logging in production
return signedJWT.serialize()
}
private fun String.base64Url(): String {
return Base64.getUrlEncoder().withoutPadding().encodeToString(toByteArray())
}
private fun buildPublicRsaJwkFromAndroid(alias: String): RSAKey {
val ks = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val cert = ks.getCertificate(alias)
?: throw IllegalStateException("No certificate for alias: $alias")
val publicKey = cert.publicKey as? RSAPublicKey
?: throw IllegalStateException("Alias $alias is not an RSA key")
return RSAKey.Builder(publicKey)
.keyID(alias)
.build()
}
private fun buildPublicEcJwkFromAndroid(alias: String): ECKey {
val ks = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val cert = ks.getCertificate(alias)
?: throw IllegalStateException("No certificate for alias: $alias")
val publicKey = cert.publicKey as? ECPublicKey
?: throw IllegalStateException("Alias $alias is not an EC key")
return ECKey.Builder(Curve.P_256, publicKey)
.keyID(alias)
.build()
}
private fun loadPrivateKey(alias: String): PrivateKey {
val ks = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
return ks.getKey(alias, null) as? PrivateKey
?: throw IllegalStateException("Private key not found for alias: $alias")
}
suspend fun sendTokenRequest(
tokenRequest: TokenRequest,
tokenEndpoint: String
): JSONObject {
val url = URL(tokenEndpoint)
val conn = url.openConnection() as HttpURLConnection
conn.requestMethod = "POST"
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
conn.doOutput = true
conn.connectTimeout = 15000
conn.readTimeout = 15000
// Helper function to URL-encode parameter values
fun enc(value: String): String = URLEncoder.encode(value, "UTF-8")
val formBody = buildString {
append("grant_type=${enc(tokenRequest.grantType.value)}")
tokenRequest.authCode?.let { append("&code=${enc(it)}") }
tokenRequest.preAuthCode?.let { append("&pre-authorized_code=${enc(it)}") }
tokenRequest.txCode?.let { append("&tx_code=${enc(it)}") }
tokenRequest.clientId?.let { append("&client_id=${enc(it)}") }
tokenRequest.redirectUri?.let { append("&redirect_uri=${enc(it)}") }
tokenRequest.codeVerifier?.let { append("&code_verifier=${enc(it)}") }
}
try {
conn.outputStream.use { os ->
os.write(formBody.toByteArray())
}
val responseCode = conn.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
val responseText = conn.inputStream.bufferedReader().readText()
return JSONObject(responseText)
} else {
val errorText = conn.errorStream?.bufferedReader()?.readText() ?: "Unknown error"
throw Exception("HTTP error $responseCode: $errorText")
}
} catch (e: Exception) {
throw e
} finally {
conn.disconnect()
}
}

View File

@@ -0,0 +1,312 @@
package com.example.samplecredentialwallet.ui.credential
import android.graphics.BitmapFactory
import android.util.Base64
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.asImageBitmap
import androidx.compose.ui.layout.ContentScale
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.example.samplecredentialwallet.utils.CredentialStore
import com.example.samplecredentialwallet.utils.CredentialParser
import org.json.JSONArray
import org.json.JSONObject
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CredentialListScreen(navController: NavController, credentialIndex: Int = -1) {
val credentials = CredentialStore.getAllCredentials()
val displayCredentials = if (credentialIndex >= 0 && credentialIndex < credentials.size) {
listOf(credentials[credentialIndex])
} else {
credentials
}
var selectedCredentialIndex by remember { mutableStateOf<Int?>(0) }
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
"ID Details",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
},
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
},
actions = {
IconButton(onClick = { /* Help */ }) {
Icon(Icons.Default.Info, contentDescription = "Help")
}
IconButton(onClick = { /* More options */ }) {
Icon(Icons.Default.MoreVert, contentDescription = "More")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
)
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
if (displayCredentials.isEmpty()) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
"No credentials downloaded yet",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"Download a credential to see it here",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
)
}
} else {
// Show credentials
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
itemsIndexed(displayCredentials) { index, credential ->
CredentialCard(
credential = credential,
index = index,
isExpanded = selectedCredentialIndex == index,
onToggle = {
selectedCredentialIndex = if (selectedCredentialIndex == index) null else index
}
)
}
}
}
}
}
}
@Composable
fun CredentialCard(
credential: String,
index: Int,
isExpanded: Boolean,
onToggle: () -> Unit
) {
val parsedData = remember(credential) { parseCredential(credential) }
Column(
modifier = Modifier.fillMaxWidth()
) {
Card(
modifier = Modifier
.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = com.example.samplecredentialwallet.ui.theme.CardBlue
),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Top
) {
parsedData["faceImage"]?.let { base64Image ->
val bitmap = remember(base64Image) {
try {
val imageBytes = Base64.decode(base64Image, Base64.DEFAULT)
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
} catch (e: Exception) {
null
}
}
bitmap?.let {
Image(
bitmap = it.asImageBitmap(),
contentDescription = "Profile Photo",
modifier = Modifier
.size(100.dp)
.clip(androidx.compose.foundation.shape.RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop
)
}
}
}
Spacer(modifier = Modifier.height(20.dp))
// Display ALL fields from received credential data
parsedData.forEach { (key, value) ->
if (key != "faceImage" && key != "type" && key != "error" && key != "raw") {
CredentialFieldInCard(
label = key.replaceFirstChar { it.uppercase() }.replace(Regex("([a-z])([A-Z])"), "$1 $2"),
value = value
)
Spacer(modifier = Modifier.height(12.dp))
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
parsedData["activationPending"]?.let {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFFFF3E0)
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = null,
tint = Color(0xFFFFA726),
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(
text = "Activation pending for online login",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold
)
Text(
text = "Please click the button below to activate this credential to be used for online login.",
style = MaterialTheme.typography.bodySmall
)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = { /* Handle activation */ },
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = com.example.samplecredentialwallet.ui.theme.InjiOrange
)
) {
Text("Activate", fontSize = 16.sp)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Show Raw Data Button
var showRaw by remember { mutableStateOf(false) }
OutlinedButton(
onClick = { showRaw = !showRaw },
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = com.example.samplecredentialwallet.ui.theme.InjiPurple
)
) {
Icon(
imageVector = if (showRaw) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(if (showRaw) "Hide Raw Data" else "Show Raw Data")
}
if (showRaw) {
Spacer(modifier = Modifier.height(8.dp))
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 300.dp)
.verticalScroll(rememberScrollState())
.padding(12.dp)
) {
Text(
text = credential,
style = MaterialTheme.typography.bodySmall,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace
)
}
}
}
}
}
@Composable
fun CredentialFieldInCard(label: String, value: String) {
Column {
Text(
text = label,
style = MaterialTheme.typography.bodySmall,
color = Color.White.copy(alpha = 0.8f),
fontSize = 12.sp
)
Text(
text = value,
style = MaterialTheme.typography.bodyLarge,
color = Color.White,
fontSize = 16.sp
)
}
}
private fun parseCredential(credentialJson: String): Map<String, String> {
return CredentialParser.parseCredential(credentialJson)
}

View File

@@ -0,0 +1,357 @@
package com.example.samplecredentialwallet.ui.home
import android.graphics.BitmapFactory
import android.util.Base64
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.samplecredentialwallet.R
import com.example.samplecredentialwallet.ui.theme.CardBlue
import com.example.samplecredentialwallet.ui.theme.InjiOrange
import com.example.samplecredentialwallet.utils.CredentialParser
import com.example.samplecredentialwallet.utils.CredentialStore
import com.example.samplecredentialwallet.utils.CredentialVerifier
import kotlinx.coroutines.launch
import org.json.JSONArray
import org.json.JSONObject
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
onNavigate: () -> Unit,
onViewCredential: (Int) -> Unit = {}
) {
val credentials = remember { mutableStateOf(CredentialStore.getAllCredentials()) }
val verificationStatus = remember { mutableStateMapOf<Int, Boolean?>() }
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(Unit) {
credentials.value = CredentialStore.getAllCredentials()
}
// Verify credentials when they change
LaunchedEffect(credentials.value) {
credentials.value.forEachIndexed { index, credential ->
if (!verificationStatus.containsKey(index)) {
verificationStatus[index] = null // Start as unverified
coroutineScope.launch {
val isValid = CredentialVerifier.verifyCredential(credential, demoMode = true)
verificationStatus[index] = isValid
}
}
}
}
Scaffold(
floatingActionButton = {
FloatingActionButton(
onClick = onNavigate,
containerColor = InjiOrange,
contentColor = Color.White,
shape = CircleShape,
modifier = Modifier
.size(56.dp)
.offset(y = (-40).dp)
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add New Card",
modifier = Modifier.size(28.dp)
)
}
},
floatingActionButtonPosition = androidx.compose.material3.FabPosition.End
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// Background
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFF5F5F5))
)
Column(
modifier = Modifier
.fillMaxSize()
) {
// Header with Title
Box(
modifier = Modifier
.fillMaxWidth()
.background(Color.White)
.padding(16.dp)
) {
Text(
text = "Sample Credential Wallet",
style = MaterialTheme.typography.titleLarge,
color = Color.Gray,
fontSize = 20.sp
)
// Clear All button (only show if there are credentials)
if (credentials.value.isNotEmpty()) {
TextButton(
onClick = {
CredentialStore.clearCredentials()
credentials.value = CredentialStore.getAllCredentials()
},
modifier = Modifier.align(Alignment.TopEnd)
) {
Text(
text = "Clear All",
color = InjiOrange,
fontWeight = FontWeight.Medium,
fontSize = 14.sp
)
}
}
}
// Credential count
Text(
text = "${credentials.value.size} card${if (credentials.value.size != 1) "s" else ""}",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
// Credentials list
if (credentials.value.isEmpty()) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "No cards yet",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Medium,
color = Color.Gray
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Tap + to add a new card",
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray
)
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
itemsIndexed(credentials.value) { index, credential ->
CredentialHomeCard(
credential = credential,
isValid = verificationStatus[index],
onClick = { onViewCredential(index) }
)
}
}
}
}
}
}
}
@Composable
fun CredentialHomeCard(
credential: String,
isValid: Boolean? = null,
onClick: () -> Unit
) {
val parsedData = remember(credential) { parseCredential(credential) }
val vcTypeName = parsedData["credentialName"] ?: parsedData["type"]?.replace("VerifiableCredential", "Verifiable Credential") ?: "Verifiable Credential"
Card(
modifier = Modifier
.fillMaxWidth()
.height(110.dp)
.clickable { onClick() },
colors = CardDefaults.cardColors(
containerColor = CardBlue
),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
val faceImage = parsedData["faceImage"]
if (faceImage != null) {
val bitmap = remember(faceImage) {
try {
val imageBytes = Base64.decode(faceImage, Base64.DEFAULT)
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
} catch (e: Exception) {
null
}
}
bitmap?.let {
Image(
bitmap = it.asImageBitmap(),
contentDescription = "Profile Photo",
modifier = Modifier
.size(60.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
} ?: run {
Box(
modifier = Modifier
.size(60.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.3f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.Person,
contentDescription = "Profile Icon",
modifier = Modifier.size(36.dp),
tint = Color.White
)
}
}
} else {
Box(
modifier = Modifier
.size(60.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.3f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.Person,
contentDescription = "Profile Icon",
modifier = Modifier.size(36.dp),
tint = Color.White
)
}
}
Spacer(modifier = Modifier.width(10.dp))
// Credential Info
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.SpaceEvenly
) {
Text(
text = vcTypeName,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = Color.White,
fontSize = 14.sp,
maxLines = 1
)
Spacer(modifier = Modifier.height(4.dp))
// Verification status badge
Row(verticalAlignment = Alignment.CenterVertically) {
when (isValid) {
true -> {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = "Verified",
tint = Color(0xFF4CAF50),
modifier = Modifier.size(14.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "Valid",
style = MaterialTheme.typography.bodySmall,
color = Color.White,
fontSize = 11.sp
)
}
false -> {
Icon(
imageVector = Icons.Default.Info,
contentDescription = "Not Verified",
tint = Color(0xFFFF9800),
modifier = Modifier.size(14.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "Unverified",
style = MaterialTheme.typography.bodySmall,
color = Color.White,
fontSize = 11.sp
)
}
null -> {
Icon(
imageVector = Icons.Default.Info,
contentDescription = "Checking",
tint = Color(0xFFBDBDBD),
modifier = Modifier.size(14.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "Checking...",
style = MaterialTheme.typography.bodySmall,
color = Color.White,
fontSize = 11.sp
)
}
}
}
}
parsedData["activationPending"]?.let {
Surface(
color = Color(0xFFFFA726),
shape = CircleShape,
modifier = Modifier.size(24.dp)
) {
Box(contentAlignment = Alignment.Center) {
Text(
text = "!",
color = Color.White,
fontWeight = FontWeight.Bold
)
}
}
}
}
}
}
private fun parseCredential(credentialJson: String): Map<String, String> {
return CredentialParser.parseCredential(credentialJson)
}

View File

@@ -0,0 +1,46 @@
package com.example.samplecredentialwallet.ui.issuer
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.samplecredentialwallet.viewmodel.CredentialViewModel
@Composable
fun IssuerDetailScreen(
issuerType: String,
onNavigateNext: () -> Unit,
vm: CredentialViewModel = viewModel()
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
when (issuerType) {
"mosip" -> {
Text("MOSIP Issuer", style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(20.dp))
Button(onClick = {
vm.setNationalIdentityConstants()
onNavigateNext()
}) {
Text("National Identity")
}
}
"stay_protected" -> {
Text("Stay Protected Issuer", style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(20.dp))
Button(onClick = {
vm.setLifeInsuranceConstants()
onNavigateNext()
}) {
Text("Life Insurance")
}
}
}
}
}

View File

@@ -0,0 +1,193 @@
package com.example.samplecredentialwallet.ui.issuer
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.samplecredentialwallet.R
data class Issuer(
val id: String,
val name: String,
val description: String,
val logoRes: Int
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun IssuerListScreen(onIssuerClick: (String) -> Unit) {
var searchQuery by remember { mutableStateOf("") }
val issuers = listOf(
Issuer(
id = "Mosip",
name = "Republic of Veridonia National ID Department",
description = "Download National ID credential from Collab environment",
logoRes = R.drawable.veridonia_logo
),
Issuer(
id = "StayProtected",
name = "StayProtected Insurance",
description = "Download insurance credential from Collab environment",
logoRes = R.drawable.stay_protected_logo
),
Issuer(
id = "MosipTAN",
name = "Republic of Veridonia Tax Department",
description = "Download Tax ID credential from Collab environment",
logoRes = R.drawable.tan_logo
),
Issuer(
id = "Land",
name = "AgroVeritas Property & Land Registry",
description = "Download Land Registry credential from Collab environment",
logoRes = R.drawable.agro_vertias_logo
)
)
// Filter issuers based on search query
val filteredIssuers = if (searchQuery.isEmpty()) {
issuers
} else {
issuers.filter { issuer ->
issuer.name.contains(searchQuery, ignoreCase = true) ||
issuer.description.contains(searchQuery, ignoreCase = true)
}
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
"Add new card",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
)
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// Search bar
OutlinedTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
placeholder = { Text("Search by Issuer's name") },
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
colors = OutlinedTextFieldDefaults.colors(
unfocusedContainerColor = Color.White,
focusedContainerColor = Color.White
),
singleLine = true
)
// Issuer List
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(filteredIssuers) { issuer ->
IssuerCard(issuer = issuer, onClick = { onIssuerClick(issuer.id) })
}
// Show "No results" message if search yields nothing
if (filteredIssuers.isEmpty() && searchQuery.isNotEmpty()) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
contentAlignment = Alignment.Center
) {
Text(
"No issuers found matching \"$searchQuery\"",
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray
)
}
}
}
}
}
}
}
@Composable
fun IssuerCard(issuer: Issuer, onClick: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() },
colors = CardDefaults.cardColors(
containerColor = Color.White
),
border = BorderStroke(1.dp, Color.LightGray),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Issuer Logo
Image(
painter = painterResource(id = issuer.logoRes),
contentDescription = "${issuer.name} Logo",
modifier = Modifier
.size(48.dp)
.padding(2.dp),
contentScale = ContentScale.Fit
)
Spacer(modifier = Modifier.width(12.dp))
// Issuer Info
Column(modifier = Modifier.weight(1f)) {
Text(
text = issuer.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
fontSize = 14.sp
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = issuer.description,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray,
fontSize = 11.sp
)
}
}
}
}

View File

@@ -0,0 +1,38 @@
package com.example.samplecredentialwallet.ui.splash
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
@Composable
fun SplashScreen(onTimeout: () -> Unit) {
LaunchedEffect(Unit) {
delay(2000) // Show splash for 2 seconds
onTimeout()
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
contentAlignment = Alignment.Center
) {
Text(
text = "Sample Credential Wallet",
style = MaterialTheme.typography.headlineLarge,
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
color = Color.Gray
)
}
}

View File

@@ -0,0 +1,32 @@
package com.example.samplecredentialwallet.ui.theme
import androidx.compose.ui.graphics.Color
// Inji Colors - Primary Orange
val InjiOrange = Color(0xFFF2680C)
val InjiOrangeLight = Color(0xFFFF8C42)
val InjiOrangeDark = Color(0xFFD95B0A)
// Inji Colors - Primary Purple
val InjiPurple = Color(0xFF451691)
val InjiPurpleLight = Color(0xFF6B7FD7)
val InjiPurpleDark = Color(0xFF2B0B5C)
// Secondary Colors
val InjiBlue = Color(0xFF687FD7)
val InjiPink = Color(0xFF95F1F0)
val InjiGray = Color(0xFFAEA0B3)
// Background Colors
val InjiBackgroundLight = Color(0xFFFAFAFA)
val InjiBackgroundDark = Color(0xFF1A1A2E)
val InjiSurfaceLight = Color(0xFFFFFFFF)
val InjiSurfaceDark = Color(0xFF2D2D44)
// Gradient Colors
val GradientStart = Color(0xFFF2680C) // Orange
val GradientEnd = Color(0xFF451691) // Purple
// Card Color (Blue from the design)
val CardBlue = Color(0xFF451691)
val CardBlueDark = Color(0xFF2D0B5C)

View File

@@ -0,0 +1,75 @@
package com.example.samplecredentialwallet.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = InjiOrange,
secondary = InjiPurple,
tertiary = InjiBlue,
background = InjiBackgroundDark,
surface = InjiSurfaceDark,
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color.White,
onSurface = Color.White
)
private val LightColorScheme = lightColorScheme(
primary = InjiOrange,
secondary = InjiPurple,
tertiary = InjiBlue,
background = InjiBackgroundLight,
surface = InjiSurfaceLight,
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F)
)
@Composable
fun SampleCredentialWalletTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = false,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.background.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@@ -0,0 +1,34 @@
package com.example.samplecredentialwallet.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

View File

@@ -0,0 +1,58 @@
package com.example.samplecredentialwallet.utils
import android.util.Log
import kotlinx.coroutines.CompletableDeferred
object AuthCodeHolder {
private var deferred: CompletableDeferred<String?>? = null
private var lastCode: String? = null
@Synchronized
fun prepare(): CompletableDeferred<String?> {
if (deferred == null || deferred?.isCompleted == true) {
deferred = CompletableDeferred()
Log.d("AuthCodeHolder", "New deferred created")
// If we have a buffered code, complete immediately
if (lastCode != null) {
Log.d("AuthCodeHolder", "Fulfilling deferred with buffered code")
deferred?.complete(lastCode)
lastCode = null
}
} else {
Log.d("AuthCodeHolder", "Reusing existing deferred")
}
return deferred!!
}
@Synchronized
fun complete(code: String?) {
Log.d("AuthCodeHolder", "Completing auth code")
val currentDeferred = deferred
if (currentDeferred != null && !currentDeferred.isCompleted) {
currentDeferred.complete(code)
Log.d("AuthCodeHolder", "Deferred completed successfully")
} else {
// Buffer the code for future prepare() call
lastCode = code
Log.d("AuthCodeHolder", "Code buffered for future deferred")
}
}
// This is what authorizeUser will call
suspend fun waitForCode(): String {
Log.d("AuthCodeHolder", "Waiting for auth code...")
val d = prepare()
val result = d.await()
Log.d("AuthCodeHolder", "Auth code received")
return result ?: throw Exception("Auth canceled or failed")
}
@Synchronized
fun reset() {
Log.d("AuthCodeHolder", "Resetting AuthCodeHolder")
deferred?.cancel()
deferred = null
lastCode = null
}
}

View File

@@ -0,0 +1,9 @@
package com.example.samplecredentialwallet.utils
object Constants {
var credentialIssuerHost: String? = null
var credentialTypeId: String? = null
var clientId: String? = null
var redirectUri: String? = null
var credentialDisplayName: String? = null
}

View File

@@ -0,0 +1,104 @@
package com.example.samplecredentialwallet.utils
import org.json.JSONArray
import org.json.JSONObject
object CredentialParser {
fun parseCredential(credentialJson: String): Map<String, String> {
val result = mutableMapOf<String, String>()
val addedKeys = mutableSetOf<String>()
try {
val cleanCredential = if (credentialJson.startsWith("CredentialResponse(")) {
val credMatch = Regex("""credential=(\{.*\})(?:,|\))""").find(credentialJson)
credMatch?.groupValues?.get(1) ?: credentialJson
} else {
credentialJson
}
val json = JSONObject(cleanCredential)
val types = json.optJSONArray("type")
if (types != null && types.length() > 0) {
result["type"] = types.getString(types.length() - 1)
}
fun shouldSkipValue(value: String): Boolean {
return value.isEmpty() ||
value == "N/A" ||
value.length > 100 ||
value.startsWith("http://") ||
value.startsWith("https://") ||
value.startsWith("did:") ||
value.contains("eyJ") ||
value.matches(Regex("^[A-Za-z0-9+/=]{50,}$"))
}
fun addField(key: String, value: String) {
val keyLower = key.lowercase()
if (keyLower !in addedKeys && !shouldSkipValue(value)) {
result[key] = value
addedKeys.add(keyLower)
}
}
val credentialSubject = json.optJSONObject("credentialSubject")
if (credentialSubject != null) {
credentialSubject.keys().forEach { key ->
val value = credentialSubject.opt(key)
when (value) {
is String -> {
if (key == "face" && value.startsWith("data:image/")) {
result["faceImage"] = value.substringAfter(",")
} else if (value.trim().startsWith("{") && value.contains("\"language\"") && value.contains("\"value\"")) {
try {
val langObj = JSONObject(value)
addField(key, langObj.optString("value", ""))
} catch (e: Exception) {
addField(key, value)
}
} else {
addField(key, value)
}
}
is JSONObject -> {
val actualValue = value.optString("value", "")
if (actualValue.isNotEmpty()) {
val cleanedValue = actualValue.replace(Regex("(eng|hin|ara|fra|deu|spa|por|rus|zho|jpn|kor)$"), "")
addField(key, cleanedValue)
} else {
addField(key, value.toString())
}
}
is JSONArray -> {
val arrayValues = mutableListOf<String>()
for (i in 0 until value.length()) {
when (val item = value.opt(i)) {
is JSONObject -> {
val langValue = item.optString("value", "")
if (langValue.isNotEmpty()) {
arrayValues.add(langValue.replace(Regex("(eng|hin|ara|fra|deu|spa|por|rus|zho|jpn|kor)$"), ""))
} else {
arrayValues.add(item.toString())
}
}
is String -> arrayValues.add(item)
else -> arrayValues.add(item.toString())
}
}
addField(key, arrayValues.joinToString(", "))
}
else -> addField(key, value.toString())
}
}
}
} catch (e: Exception) {
result["error"] = "Failed to parse credential"
result["raw"] = credentialJson.take(100) + "..."
}
return result
}
}

View File

@@ -0,0 +1,18 @@
package com.example.samplecredentialwallet.utils
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshots.SnapshotStateList
object CredentialStore {
val credentials: SnapshotStateList<String> = mutableStateListOf()
fun addCredential(credentialJson: String) {
credentials.add(credentialJson)
}
fun getAllCredentials(): List<String> = credentials
fun clearCredentials() {
credentials.clear()
}
}

View File

@@ -0,0 +1,126 @@
package com.example.samplecredentialwallet.utils
import android.util.Log
import io.mosip.vercred.vcverifier.CredentialsVerifier
import io.mosip.vercred.vcverifier.constants.CredentialFormat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
object CredentialVerifier {
private val verifier = CredentialsVerifier()
private const val LOG_TAG = "CredentialVerifier"
suspend fun verifyCredential(credentialJson: String, demoMode: Boolean = false): Boolean {
return withContext(Dispatchers.IO) {
try {
Log.d(LOG_TAG, "Starting credential verification (demoMode: $demoMode)")
val credentialHash = credentialJson.hashCode().toString(16)
Log.d(LOG_TAG, "Credential hash: $credentialHash, length: ${credentialJson.length}")
val cleanCredential = if (credentialJson.startsWith("CredentialResponse(")) {
Log.d(LOG_TAG, "Extracting credential from response wrapper")
val credMatch = Regex("""credential=(\{.*\})(?:,|\))""").find(credentialJson)
credMatch?.groupValues?.get(1) ?: credentialJson
} else {
credentialJson
}
// Validate JSON structure
try {
JSONObject(cleanCredential)
Log.d(LOG_TAG, "JSON structure valid")
} catch (e: Exception) {
Log.e(LOG_TAG, "Invalid JSON structure: ${e.message}")
return@withContext false
}
// Perform cryptographic verification
Log.d(LOG_TAG, "Performing cryptographic verification with LDP_VC format")
try {
val result = verifier.verify(cleanCredential, CredentialFormat.LDP_VC)
if (result.verificationStatus) {
Log.i(LOG_TAG, "✓ Credential verified successfully")
Log.d(LOG_TAG, "Verification message: ${result.verificationMessage}")
return@withContext true
} else {
Log.w(LOG_TAG, "✗ Credential verification failed")
Log.w(LOG_TAG, "Error code: ${result.verificationErrorCode}")
Log.w(LOG_TAG, "Error message: ${result.verificationMessage}")
if (demoMode) {
Log.i(LOG_TAG, "Demo mode: accepting despite verification failure")
return@withContext true
} else {
Log.e(LOG_TAG, "Production mode: rejecting unverified credential")
return@withContext false
}
}
} catch (verifyError: NoClassDefFoundError) {
Log.w(LOG_TAG, "Verification library class not found: ${verifyError.message}")
if (demoMode) {
Log.i(LOG_TAG, "Demo mode: accepting despite library error")
return@withContext true
} else {
Log.e(LOG_TAG, "Production mode: cannot verify without library")
return@withContext false
}
} catch (verifyError: ClassNotFoundException) {
Log.w(LOG_TAG, "Verification library dependencies missing: ${verifyError.message}")
if (demoMode) {
Log.i(LOG_TAG, "Demo mode: accepting despite missing dependencies")
return@withContext true
} else {
Log.e(LOG_TAG, "Production mode: cannot verify without dependencies")
return@withContext false
}
} catch (verifyError: UnsatisfiedLinkError) {
Log.w(LOG_TAG, "Native library error: ${verifyError.message}")
if (demoMode) {
Log.i(LOG_TAG, "Demo mode: accepting despite native library error")
return@withContext true
} else {
Log.e(LOG_TAG, "Production mode: cannot verify without native library")
return@withContext false
}
} catch (verifyError: Exception) {
Log.e(LOG_TAG, "Verification exception: ${verifyError.javaClass.simpleName} - ${verifyError.message}")
if (demoMode) {
Log.i(LOG_TAG, "Demo mode: accepting despite verification exception")
return@withContext true
} else {
Log.e(LOG_TAG, "Production mode: rejecting due to verification exception")
return@withContext false
}
}
} catch (e: Exception) {
Log.e(LOG_TAG, "Unexpected error during verification: ${e.javaClass.simpleName} - ${e.message}")
if (demoMode) {
Log.w(LOG_TAG, "Demo mode: attempting fallback validation")
// In demo mode, at least validate it's parseable JSON
return@withContext try {
val cleanCredential = if (credentialJson.startsWith("CredentialResponse(")) {
val credMatch = Regex("""credential=(\{.*\})(?:,|\))""").find(credentialJson)
credMatch?.groupValues?.get(1) ?: credentialJson
} else {
credentialJson
}
JSONObject(cleanCredential)
Log.i(LOG_TAG, "Demo mode: valid JSON structure, accepting")
true
} catch (jsonError: Exception) {
Log.e(LOG_TAG, "Demo mode: invalid JSON, rejecting")
false
}
} else {
Log.e(LOG_TAG, "Production mode: rejecting due to unexpected error")
return@withContext false
}
}
}
}
}

View File

@@ -0,0 +1,22 @@
package com.example.samplecredentialwallet.utils
import io.mosip.vciclient.token.TokenRequest
object EndpointConfig {
private val tokenEndpointMappings = mapOf(
"tan" to "https://api.collab.mosip.net/v1/mimoto/get-token/MosipTAN",
"esignet-mosipid" to "https://api.collab.mosip.net/residentmobileapp/get-token/Mosip",
"esignet-insurance" to "https://api.collab.mosip.net/residentmobileapp/get-token/StayProtected",
"esignet-mock" to "https://api.collab.mosip.net/v1/mimoto/get-token/Land"
)
fun resolveTokenEndpoint(tokenEndpoint: String, credentialIssuerHost: String?): String {
return when {
credentialIssuerHost?.contains("tan") == true -> tokenEndpointMappings["tan"]
tokenEndpoint.contains("esignet-mosipid") -> tokenEndpointMappings["esignet-mosipid"]
tokenEndpoint.contains("esignet-insurance") -> tokenEndpointMappings["esignet-insurance"]
tokenEndpoint.contains("esignet-mock") -> tokenEndpointMappings["esignet-mock"]
else -> throw Exception("Unknown token endpoint: $tokenEndpoint")
} ?: throw Exception("No mapping found")
}
}

View File

@@ -0,0 +1,58 @@
package com.example.samplecredentialwallet.utils
data class IssuerConfiguration(
val credentialIssuerHost: String,
val credentialTypeId: String,
val clientId: String,
val redirectUri: String,
val credentialDisplayName: String
)
object IssuerRepository {
private val configurations = mapOf(
"Mosip" to IssuerConfiguration(
credentialIssuerHost = "https://injicertify-mosipid.collab.mosip.net",
credentialTypeId = "MosipVerifiableCredential",
clientId = "mpartner-default-mimoto-mosipid-oidc",
redirectUri = "io.mosip.residentapp.inji://oauthredirect",
credentialDisplayName = "Veridonia National ID"
),
"StayProtected" to IssuerConfiguration(
credentialIssuerHost = "https://injicertify-insurance.collab.mosip.net",
credentialTypeId = "InsuranceCredential",
clientId = "esignet-sunbird-partner",
redirectUri = "io.mosip.residentapp.inji://oauthredirect",
credentialDisplayName = "Life Insurance"
),
"MosipTAN" to IssuerConfiguration(
credentialIssuerHost = "https://injicertify-tan.collab.mosip.net/v1/certify/issuance",
credentialTypeId = "IncomeTaxAccountCredential",
clientId = "mpartner-default-mimoto-mosipid-oidc",
redirectUri = "io.mosip.residentapp.inji://oauthredirect",
credentialDisplayName = "Income Tax Account"
),
"Land" to IssuerConfiguration(
credentialIssuerHost = "https://injicertify-landregistry.collab.mosip.net",
credentialTypeId = "LandStatementCredential",
clientId = "mpartner-default-mimoto-land-oidc",
redirectUri = "io.mosip.residentapp.inji://oauthredirect",
credentialDisplayName = "Land Records Statement"
)
)
fun getConfiguration(issuerType: String): IssuerConfiguration? {
return configurations[issuerType]
}
fun applyConfiguration(issuerType: String): Boolean {
val config = getConfiguration(issuerType) ?: return false
Constants.credentialIssuerHost = config.credentialIssuerHost
Constants.credentialTypeId = config.credentialTypeId
Constants.clientId = config.clientId
Constants.redirectUri = config.redirectUri
Constants.credentialDisplayName = config.credentialDisplayName
return true
}
}

View File

@@ -0,0 +1,19 @@
package com.example.samplecredentialwallet.utils
import io.mosip.pixelpass.PixelPass
import io.mosip.pixelpass.types.ECC
class PixelPassModule {
private val pixelPass = PixelPass()
fun generateQRData(credentialData: String, header: String = ""): String {
return pixelPass.generateQRData(credentialData, header)
}
fun generateQRCode(credentialData: String, header: String = "", ecc: ECC = ECC.L): String {
return pixelPass.generateQRCode(credentialData, ecc, header)
}
}

View File

@@ -0,0 +1,232 @@
package com.example.samplecredentialwallet.utils
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import com.reactnativesecurekeystore.*
import com.reactnativesecurekeystore.biometrics.Biometrics
import kotlinx.coroutines.*
class SecureKeystoreManager(private val context: Context) {
companion object {
private const val TAG = "SecureKeystoreManager"
private const val PREFS_NAME = "keystore_prefs"
private const val KEY_KEYS_GENERATED = "keys_generated"
private const val KEY_ORDER_PREFERENCE = "keyPreference"
@Volatile
private var INSTANCE: SecureKeystoreManager? = null
fun getInstance(context: Context): SecureKeystoreManager {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: SecureKeystoreManager(context.applicationContext).also { INSTANCE = it }
}
}
}
enum class KeyType(val value: String) {
RS256("RS256"),
ES256("ES256")
}
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
// Initialize MOSIP components
private val keyGenerator = KeyGeneratorImpl()
private val cipherBox = CipherBoxImpl()
private val biometrics = Biometrics()
private val preferences = PreferencesImpl(context)
private val keystore = SecureKeystoreImpl(keyGenerator, cipherBox, biometrics, preferences)
private val deviceCapability = DeviceCapability(keystore, keyGenerator, biometrics)
/**
* Check if device supports hardware keystore
*/
fun isHardwareKeystoreSupported(): Boolean {
return deviceCapability.supportsHardwareKeyStore()
}
/**
* Check if biometrics is enabled on device
*/
fun isBiometricsEnabled(): Boolean {
return deviceCapability.hasBiometricsEnabled(context)
}
/**
* Check if keys have already been generated
*/
fun areKeysGenerated(): Boolean {
return prefs.getBoolean(KEY_KEYS_GENERATED, false)
}
/**
* Initialize keystore - generate keys if not already done
* Returns success/failure result
*/
suspend fun initializeKeystore(): Result<String> = withContext(Dispatchers.IO) {
if (areKeysGenerated()) {
Log.i(TAG, "Keys already generated, skipping initialization")
return@withContext Result.success("Keys already exist")
}
try {
generateAndStoreKeyPairs()
markKeysAsGenerated()
Log.i(TAG, "Keystore initialization completed successfully")
Result.success("Key pairs generated and stored successfully!")
} catch (e: Exception) {
Log.e(TAG, "Failed to initialize keystore", e)
Result.failure(e)
}
}
private suspend fun generateAndStoreKeyPairs() {
val deviceBiometricsEnabled = isBiometricsEnabled()
val isHardwareSupported = isHardwareKeystoreSupported()
Log.i(TAG, "Hardware keystore supported: $isHardwareSupported")
Log.i(TAG, "Biometrics enabled on device: $deviceBiometricsEnabled")
val isBiometricsEnabledForKeys = false
if (isHardwareSupported) {
generateKeyPairRSA(isBiometricsEnabledForKeys)
generateKeyPairECR1(isBiometricsEnabledForKeys)
} else {
Log.w(TAG, "Hardware keystore not supported, keys will be stored in software")
throw Exception("Hardware keystore not supported on this device")
}
storeKeyPreferences()
}
/**
* Generate RSA key pair (RS256)
*/
private suspend fun generateKeyPairRSA(isBiometricsEnabled: Boolean) = withContext(Dispatchers.IO) {
val alias = KeyType.RS256.value
try {
if (!keystore.hasAlias(alias)) {
val publicKeyPem = keystore.generateKeyPair(
KeyType.RS256.value,
alias,
isBiometricsEnabled,
0
)
Log.i(TAG, "Generated RS256 key pair with alias: $alias")
Log.d(TAG, "RS256 Public Key PEM: $publicKeyPem")
} else {
Log.i(TAG, "RS256 key pair already exists with alias: $alias")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to generate RS256 key pair", e)
throw e
}
}
/**
* Generate EC key pair (ES256)
*/
private suspend fun generateKeyPairECR1(isBiometricsEnabled: Boolean) = withContext(Dispatchers.IO) {
val alias = KeyType.ES256.value
try {
if (!keystore.hasAlias(alias)) {
val publicKeyPem = keystore.generateKeyPair(
KeyType.ES256.value,
alias,
isBiometricsEnabled,
0 // auth timeout
)
Log.i(TAG, "Generated ES256 key pair with alias: $alias")
Log.d(TAG, "ES256 Public Key PEM: $publicKeyPem")
} else {
Log.i(TAG, "ES256 key pair already exists with alias: $alias")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to generate ES256 key pair", e)
throw e
}
}
/**
* Store key order preferences similar to React Native
*/
private fun storeKeyPreferences() {
try {
// Create key order map similar to React Native implementation
val keyOrderJson = "{\"RS256\":\"RS256\",\"ES256\":\"ES256\"}"
// Store using the preferences implementation
preferences.savePreference(KEY_ORDER_PREFERENCE, keyOrderJson)
Log.i(TAG, "Stored key preferences: $keyOrderJson")
} catch (e: Exception) {
Log.e(TAG, "Failed to store key preferences", e)
}
}
/**
* Mark keys as generated in SharedPreferences
*/
private fun markKeysAsGenerated() {
prefs.edit().putBoolean(KEY_KEYS_GENERATED, true).apply()
}
/**
* Retrieve public key for given alias
*/
suspend fun getPublicKey(keyType: KeyType): Result<String> = withContext(Dispatchers.IO) {
try {
val alias = keyType.value
if (!keystore.hasAlias(alias)) {
throw Exception("Key not found for alias: $alias")
}
val publicKey = keystore.retrieveKey(alias)
Result.success(publicKey)
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* Check if specific key exists
*/
fun hasKey(keyType: KeyType): Boolean {
return keystore.hasAlias(keyType.value)
}
/**
* Clear all keys (use with caution)
*/
suspend fun clearAllKeys(): Result<String> = withContext(Dispatchers.IO) {
try {
keystore.removeAllKeys()
prefs.edit().clear().apply()
Log.i(TAG, "All keys cleared")
Result.success("All keys cleared successfully")
} catch (e: Exception) {
Log.e(TAG, "Failed to clear keys", e)
Result.failure(e)
}
}
/**
* Get keystore status for debugging
*/
fun getKeystoreStatus(): Map<String, Any> {
return mapOf(
"hardwareSupported" to isHardwareKeystoreSupported(),
"biometricsEnabled" to isBiometricsEnabled(),
"keysGenerated" to areKeysGenerated(),
"hasRS256" to hasKey(KeyType.RS256),
"hasES256" to hasKey(KeyType.ES256)
)
}
}

View File

@@ -0,0 +1,21 @@
package com.example.samplecredentialwallet.viewmodel
import androidx.lifecycle.ViewModel
import com.example.samplecredentialwallet.utils.Constants
class CredentialViewModel : ViewModel() {
fun setNationalIdentityConstants() {
Constants.credentialIssuerHost = "https://injicertify-mosipid.released.mosip.net"
Constants.credentialTypeId = "MOSIPVerifiableCredential"
Constants.clientId = "0VnKGbm4wF1iRVTJAJ-NbAlKNDU77vJ1ue1UUAsKRtA"
Constants.redirectUri = "io.mosip.residentapp.inji://oauthredirect"
}
fun setLifeInsuranceConstants() {
Constants.credentialIssuerHost = "https://injicertify-insurance.released.mosip.net"
Constants.credentialTypeId = "LifeInsuranceCredential"
Constants.clientId = "mpartner-default-mimoto-insurance-oidc"
Constants.redirectUri = "io.mosip.residentapp.inji://oauthredirect"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -0,0 +1,21 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/authUrlText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="No URL received yet"
android:textSize="16sp"
android:padding="8dp" />
<Button
android:id="@+id/openUrlButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Open Auth URL"
android:visibility="gone"/>
</LinearLayout>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<!-- Inji Colors -->
<color name="inji_orange">#F2680C</color>
<color name="inji_purple">#451691</color>
<color name="splash_background">#FFFFFF</color>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Sample Credential Wallet</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.SampleCredentialWallet" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@@ -0,0 +1,17 @@
package com.example.samplecredentialwallet
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@@ -0,0 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}

View File

@@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

View File

@@ -0,0 +1,32 @@
[versions]
agp = "8.9.0"
kotlin = "2.0.21"
coreKtx = "1.16.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.9.2"
activityCompose = "1.10.1"
composeBom = "2024.09.00"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

View File

@@ -0,0 +1,6 @@
#Mon Aug 25 14:38:49 IST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,27 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven("https://central.sonatype.com/repository/maven-snapshots/")
maven("https://repo.danubetech.com/repository/maven-public/")
maven("https://jitpack.io")
}
}
rootProject.name = "SampleCredentialWallet"
include(":app")