Add Kuira to an Android project¶
Outcome: your existing Android app builds against the Kuira SDK,
recognises your passkey domain, and has the Hilt graph wired so any
@HiltViewModel consuming SDK panels resolves cleanly.
Prerequisites¶
- An Android project with
minSdk ≥ 30. - A domain you control that will serve as your passkey relying-party
identifier (e.g.
yourapp.example.com). You need write access to host.well-known/assetlinks.jsonon it.
Version pin matrix¶
The SDK is built against the toolchain below; using a newer Kotlin without a matching KSP version is the #1 cause of a build error. Pin to these exact versions for the current SDK release; when the next alpha bumps a toolchain version, this table updates and the SDK pin matrix bumps with it.
| Tool | Version | Notes |
|---|---|---|
| AGP | 8.13.2 |
8.13.x minimum |
| Kotlin | 2.3.20 |
Matched KSP below |
| KSP | 2.3.6 |
Plugin id com.google.devtools.ksp |
| Hilt | 2.58 |
Both hilt-android and hilt-compiler |
| Compose BOM | 2026.03.01 |
If you use Compose (Recipe 2 does) |
| JDK | 17 |
sourceCompatibility / targetCompatibility / jvmTarget |
compileSdk |
36 |
35 works; the SDK was built against 36 |
minSdk |
30 |
Mandatory — biometric Keystore APIs |
Step 1 — Add the dependency¶
First make sure your root settings.gradle.kts declares mavenCentral()
in both pluginManagement (so the Contract plugin from Recipe 3 can
resolve) and dependencyResolutionManagement (so the runtime
artifacts resolve):
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "your-app"
include(":app")
Then add the SDK to your app module's dependencies:
dependencies {
// One line; pulls in midnight-sdk, wallet-runtime, identity,
// compact-engine, designsystem, … as transitive api dependencies.
implementation("io.github.kuiralabs:dapp-ui:0.1.0-alpha03")
// Hilt — required (the SDK is Hilt-first).
implementation("com.google.dagger:hilt-android:2.58")
ksp("com.google.dagger:hilt-compiler:2.58")
// hilt-navigation-compose for `hiltViewModel()` — needed by
// Recipe 2 to obtain the SigilStatusPanel's ViewModel.
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
// Compose — required if you'll render the SDK's panels.
implementation(platform("androidx.compose:compose-bom:2026.03.01"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.activity:activity-compose:1.12.2")
// FragmentActivity — the SDK's biometric prompts need an
// Activity that subclasses FragmentActivity (AppCompatActivity
// qualifies; ComponentActivity does not).
implementation("androidx.fragment:fragment-ktx:1.8.4")
implementation("androidx.appcompat:appcompat:1.6.1")
}
dependencies {
implementation 'io.github.kuiralabs:dapp-ui:0.1.0-alpha03'
implementation 'com.google.dagger:hilt-android:2.58'
ksp 'com.google.dagger:hilt-compiler:2.58'
implementation 'androidx.hilt:hilt-navigation-compose:1.1.0'
implementation platform('androidx.compose:compose-bom:2026.03.01')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.activity:activity-compose:1.12.2'
implementation 'androidx.fragment:fragment-ktx:1.8.4'
implementation 'androidx.appcompat:appcompat:1.6.1'
}
And declare the plugins at the root build.gradle.kts:
plugins {
id("com.android.application") version "8.13.2" apply false
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.20" apply false
id("com.google.devtools.ksp") version "2.3.6" apply false
id("com.google.dagger.hilt.android") version "2.58" apply false
}
Apply them in the app module:
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.devtools.ksp")
id("com.google.dagger.hilt.android")
}
dapp-ui api-exposes the rest of the consumer SDK surface
(midnight-sdk, wallet-runtime, identity, compact-engine, …),
so you only need that one Kuira line. Hilt, Compose, and
hilt-navigation-compose are platform deps your app would need anyway.
Verify: run ./gradlew :app:dependencies | grep kuiralabs — you
should see roughly a dozen io.github.kuiralabs:* entries pulled in
transitively, all at the same version.
Step 2 — Provide your PasskeyConfig¶
The SDK requires every consumer dApp to declare its own passkey relying-party identifier (RP ID). Create a Hilt module:
package com.example.myapp.di
import com.midnight.kuira.core.identity.passkey.PasskeyConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object IdentityConfigModule {
@Provides
@Singleton
fun providePasskeyConfig(): PasskeyConfig = PasskeyConfig(
rpId = "yourapp.example.com", // (1)
rpName = "Your App", // (2)
)
}
- Use a domain you control. Passkey assertions are bound to this
value; consumers using
nel349.github.ioor another maintainer domain will silently fail PRF derivation. - User-facing label shown in the passkey prompt ("Create a passkey for Your App").
Verify: ./gradlew :app:assembleDebug should compile. Hilt's KSP
generation runs at compile time — if PasskeyConfig isn't provided,
you'll see a "missing binding for PasskeyConfig" error.
Step 3 — Host assetlinks.json on your rpId domain¶
Digital Asset Links bind your app's package name + signing fingerprint
to your passkey domain. The passkey API refuses any ceremony until
this is in place — symptoms include RP_ID_MISMATCH, PRF
authentication failed, and silent biometric-prompt dismissal.
This is its own walkthrough — picking the right repo to host the file
in is the part most people get stuck on, and it's not obvious from
the GitHub Pages URL alone. The dedicated recipe covers fingerprint
extraction, which GitHub repo backs which rpId, hosting, and
verification.
You can skip this step temporarily — your app will compile and the
SDK won't crash, but Forge will fail until assetlinks.json is
hosted and the rpId matches.
Step 4 — Enable debug cleartext for localnet (optional)¶
If you'll target a Midnight localnet during development, the indexer +
node speak over plain HTTP on 10.0.2.2 (emulator) or 127.0.0.1
(physical device with adb reverse). Add a debug-only manifest
override so Android's network security policy doesn't block them:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:usesCleartextTraffic="true" />
</manifest>
The release build is unaffected — PREPROD and mainnet use HTTPS.
Verify: running against localnet, adb logcat | grep -i "kuira\|midnight"
should show indexer connection success, not CLEARTEXT communication
not permitted.
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
Could not resolve io.github.kuiralabs:dapp-ui |
Missing mavenCentral() in settings.gradle.kts dependencyResolutionManagement.repositories. |
Add it. |
Missing binding for PasskeyConfig at Hilt KSP time |
Step 2 not done. | Create IdentityConfigModule.kt. |
Passkey not supported on this device at runtime |
Device lacks Google Password Manager (Android 13 / GMS Core 23.40.13+ required) or screen-lock is disabled. | Enable a device PIN/biometric. |
RP_ID_MISMATCH at passkey creation |
assetlinks.json is unreachable, returns wrong content-type, or contains the wrong SHA-256. |
Verify curl -I, double-check the fingerprint. |
CLEARTEXT communication not permitted |
Step 4 not done and you're targeting localnet. | Add src/debug/AndroidManifest.xml. |
What's next¶
- Set up Sigil identity — actually use the SDK to bootstrap a passkey-derived sigil session in your app.
- Deploy and call a Compact contract
— go end-to-end from a compiled
.compactto an on-chain transaction.