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>
@@ -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"
|
||||
|
||||
237
sample-application/README.md
Normal 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 Inji’s 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).
|
||||
14
sample-application/sample-application/.gitignore
vendored
Normal 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
|
||||
@@ -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
|
||||
|
||||
1
sample-application/sample-application/app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
127
sample-application/sample-application/app/build.gradle.kts
Normal 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)
|
||||
}
|
||||
21
sample-application/sample-application/app/proguard-rules.pro
vendored
Normal 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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
*/
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 49 KiB |
@@ -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>
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 854 B |
|
After Width: | Height: | Size: 51 KiB |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">Sample Credential Wallet</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.SampleCredentialWallet" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
6
sample-application/sample-application/build.gradle.kts
Normal 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
|
||||
}
|
||||
23
sample-application/sample-application/gradle.properties
Normal 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
|
||||
@@ -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" }
|
||||
|
||||
BIN
sample-application/sample-application/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
sample-application/sample-application/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
|
||||
185
sample-application/sample-application/gradlew
vendored
Normal 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" "$@"
|
||||
89
sample-application/sample-application/gradlew.bat
vendored
Normal 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
|
||||
27
sample-application/sample-application/settings.gradle.kts
Normal 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")
|
||||