mirror of
https://github.com/selfxyz/self.git
synced 2026-04-27 03:01:15 -04:00
add kmp specs (#1902)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Self SDK — Overview
|
||||
|
||||
> Last updated: 2026-03-25
|
||||
> Last updated: 2026-04-01
|
||||
> Owner: Self Engineering
|
||||
> Status: Active (WebView-first; current pass is mock-first UI migration)
|
||||
|
||||
@@ -53,9 +53,16 @@ On **March 25, 2026**, the active SDK execution changed again:
|
||||
- [ ] `WV-06` KYC result flow through verification pipeline (future logic pass)
|
||||
- [x] `BP-01` Build pipeline — bundle webview-app into native shells
|
||||
|
||||
### Active — KMP Revival
|
||||
|
||||
- [ ] `KR-01` Scope KMP Android to 3-domain native shell parity
|
||||
- [ ] `KR-02` Scope KMP iOS to 3-domain native shell parity
|
||||
- [ ] `KR-03` Validate build artifacts and test app
|
||||
|
||||
See [KMP Revival Spec](./workstreams/kmp-revival/SPEC.md) for details.
|
||||
|
||||
### Paused
|
||||
|
||||
- [x] KMP native shells retained for future reuse
|
||||
- [x] Native MRZ/NFC consolidation work retained, but no longer on the critical path
|
||||
- [x] RN native-shell packaging retained, but not part of current client delivery
|
||||
- [x] MiniPay/KMP integration sample retained, but blocked by the paused KMP path
|
||||
@@ -116,11 +123,12 @@ On **March 25, 2026**, the active SDK execution changed again:
|
||||
| Android Shell | `packages/native-shell-android/` | Deferred | Future thin Kotlin shell: keychain/crypto + WebView host | Not required for current UI migration |
|
||||
| iOS Shell | `packages/native-shell-ios/` | Deferred | Future thin Swift shell: keychain/crypto + WebView host | Not required for current UI migration |
|
||||
| Test App | `packages/sdk-test-app/` | Deferred | Future native E2E harness | Not required for current UI migration |
|
||||
| KMP Native Shell | `packages/kmp-sdk/` | Deprecated | Reference for native shell porting, replaced by native-shell-android/ios | Do not advance; use as port reference only |
|
||||
| Swift Providers | `packages/self-sdk-swift/` | Deprecated | Reference for iOS keychain/crypto porting, replaced by native-shell-ios | Do not advance; use as port reference only |
|
||||
| KMP Native Shell | `packages/kmp-sdk/` | Active | Native shell for KMP consumers — 3-domain scope (secureStorage, crypto, lifecycle) | KR-01 (Android), KR-02 (iOS), KR-03 (validate) |
|
||||
| Swift Providers | `packages/self-sdk-swift/` | Active | iOS keychain/crypto provider implementations for KMP SDK | Required by KR-02 (query param support) |
|
||||
| RN SDK | `packages/rn-sdk/` | Paused | Retained React Native shell work | Do not advance unless scope reopens |
|
||||
| Native Consolidation | `app/ios/`, `packages/mobile-sdk-alpha/ios/`, related native code | Paused | Historical native cleanup and parity track | Keep as reference only for now |
|
||||
| MiniPay Sample | `packages/kmp-minipay-sample/` | Paused | Historical KMP integration example | Resume only if KMP path returns |
|
||||
| KMP Test App | `packages/kmp-sdk-test-app/` | Active | E2E test harness for KMP SDK | Scope to 3-domain in KR-03 |
|
||||
| MiniPay Sample | `packages/kmp-minipay-sample/` | Paused | Historical KMP integration example | May resume now that KMP path is active |
|
||||
|
||||
## Scope Rules
|
||||
|
||||
@@ -151,4 +159,5 @@ On **March 25, 2026**, the active SDK execution changed again:
|
||||
- **Future native shells (Kotlin + Swift):** [Native Shells Lite Spec](./workstreams/native-shells-lite/SPEC.md) (`NSL-01`, `NSL-02`, `NSL-03`)
|
||||
- **Build pipeline:** [Build Pipeline Spec](./workstreams/build-pipeline/SPEC.md) (BP-01)
|
||||
- **Shared engine follow-ups:** [SDK Core Spec](./workstreams/sdk-core/SPEC.md)
|
||||
- **Retained KMP/RN work:** [Paused Work Index](./paused/INDEX.md)
|
||||
- **KMP revival (3-domain scope):** [KMP Revival Spec](./workstreams/kmp-revival/SPEC.md)
|
||||
- **Retained RN work:** [Paused Work Index](./paused/INDEX.md)
|
||||
|
||||
135
specs/projects/sdk/workstreams/kmp-revival/SPEC.md
Normal file
135
specs/projects/sdk/workstreams/kmp-revival/SPEC.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# KMP Revival — Implementation Spec
|
||||
|
||||
> Last updated: 2026-04-01
|
||||
> Owner: SDK / Platform
|
||||
> Parent: `../../OVERVIEW.md`
|
||||
> Status: Active
|
||||
|
||||
## Purpose
|
||||
|
||||
- You are reviving the KMP SDK (`packages/kmp-sdk/`) to provide a native shell option for consumers who already use Kotlin Multiplatform in their apps.
|
||||
- Native-shells-lite (`packages/native-shell-android/`, `packages/native-shell-ios/`) remains the option for consumers using plain Kotlin (Android) or plain Swift (iOS).
|
||||
- Both deliver the same 3-domain scope and satisfy the same bridge contract. The difference is the integration surface: KMP `expect`/`actual` with provider registry vs. standalone platform libraries.
|
||||
- NSL-04 (delegate keychain to consumers) is already solved in KMP iOS via `SdkProviderRegistry`. KR-01 extends this to Android by moving provider interfaces to `commonMain` and shipping default Android implementations (`EncryptedSharedPreferencesProvider`, `AndroidKeystoreCryptoProvider`) that consumers can use out of the box or replace.
|
||||
- Done when the KMP SDK builds, handles the 3 required bridge domains, produces publishable artifacts (AAR + XCFramework), and a test app exercises the full WebView flow.
|
||||
|
||||
## Why Offer a KMP Option
|
||||
|
||||
| Dimension | KMP SDK | Native Shells Lite |
|
||||
|-----------|---------|-------------------|
|
||||
| Target consumer | Apps using Kotlin Multiplatform | Pure Kotlin (Android) / pure Swift (iOS) apps |
|
||||
| Provider pattern | Built-in via `SdkProviderRegistry` (both platforms) | NSL-04 adds it (~500-700 LOC) |
|
||||
| Test coverage | ~10 test files | Zero tests (pending) |
|
||||
| Publishing setup | `maven-publish` configured | None yet |
|
||||
| Result types | Strongly-typed `VerificationResult` + `SelfSdkError` | Raw JSON string |
|
||||
| Config model | Separated `SelfSdkConfig` + `VerificationRequest` | Flat config with 15+ params |
|
||||
| Extra handlers | NFC, camera, biometrics available (not registered by default) | 3 domains only |
|
||||
|
||||
Both options are valid. KMP has more infrastructure already built; native-shells-lite is simpler for non-KMP consumers.
|
||||
|
||||
## Scope
|
||||
|
||||
- Scope KMP to the same 3 bridge domains as native-shells-lite: `secureStorage`, `crypto`, `lifecycle`
|
||||
- Unify provider delegation across both platforms: move `SecureStorageProvider` and `CryptoProvider` interfaces to `commonMain`, add `SdkProviderRegistry` to `commonMain`, ship default Android implementations
|
||||
- Strip NFC, camera, biometric handler registration and their dependencies (not needed for current delivery; retain code for future)
|
||||
- Close the small gaps where native shells have features KMP lacks (WebChromeClient, query params, response shapes, protocol version validation)
|
||||
- Validate build artifacts and test app
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- NFC, camera, biometrics, haptic, analytics, documents, navigation handler registration (retain code but do not register)
|
||||
- Changes to native-shells-lite (`packages/native-shell-android/`, `packages/native-shell-ios/`)
|
||||
- WebView app changes (`packages/webview-app/`)
|
||||
- Bridge protocol changes (`packages/webview-bridge/`)
|
||||
- RN app changes (`app/`)
|
||||
- `mobile-sdk-alpha` changes
|
||||
|
||||
## Invariants
|
||||
|
||||
- Bridge protocol v1 is the only coupling between native and WebView.
|
||||
- Keychain/keystore is always native-managed. No web fallbacks for secure storage.
|
||||
- Both platforms delegate secureStorage and crypto to consumer-provided providers via `SdkProviderRegistry`. Android ships default implementations; consumers can replace them.
|
||||
- Response JSON shapes must match what `webview-bridge/src/adapters/crypto.ts` and `storage.ts` expect.
|
||||
- No regressions to existing KMP test suite (`./gradlew :shared:jvmTest`).
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Depends On | Type | Status | Notes |
|
||||
|---|---|---|---|
|
||||
| `packages/webview-bridge/` | Upstream (bridge protocol) | Done | Defines message shapes and transport names |
|
||||
| `packages/webview-app/` | Upstream (WebView bundle) | Active | KMP loads this bundle |
|
||||
| Build pipeline (BP-01) | Downstream | Done | Copies webview-app dist into native assets |
|
||||
|
||||
## Backlog
|
||||
|
||||
| ID | Title | Status | Priority | Depends On | Plan | Est. LOC |
|
||||
|---|---|---|---|---|---|---|
|
||||
| KR-01 | Scope KMP Android to 3-domain parity with provider delegation | Ready | High | - | [plans/KR-01-android-parity.md](./plans/KR-01-android-parity.md) | ~600-900 |
|
||||
| KR-02 | Scope KMP iOS to 3-domain native shell parity | Ready | High | - | [plans/KR-02-ios-parity.md](./plans/KR-02-ios-parity.md) | ~200-300 |
|
||||
| KR-03 | Validate build artifacts and test app | Ready | Medium | KR-01, KR-02 | [plans/KR-03-validate-and-publish.md](./plans/KR-03-validate-and-publish.md) | ~200 |
|
||||
|
||||
Allowed statuses: `Ready`, `In Progress`, `Blocked`, `Deferred`, `Done`
|
||||
|
||||
## Completion Checklist
|
||||
|
||||
- [ ] Provider interfaces (`SecureStorageProvider`, `CryptoProvider`) live in `commonMain`
|
||||
- [ ] `SdkProviderRegistry` lives in `commonMain` with platform-scoped defaults
|
||||
- [ ] Android ships default providers (`EncryptedSharedPreferencesProvider`, `AndroidKeystoreCryptoProvider`)
|
||||
- [ ] KMP Android builds with 3-domain scope
|
||||
- [ ] KMP iOS builds with 3-domain scope
|
||||
- [ ] All existing KMP tests pass
|
||||
- [ ] Test app exercises full WebView flow on both platforms
|
||||
- [ ] AAR + XCFramework artifacts produce clean builds
|
||||
- [ ] OVERVIEW.md module table updated (KMP: Active alongside native-shells-lite)
|
||||
|
||||
## Reference Implementations
|
||||
|
||||
| What | Source | Notes |
|
||||
|---|---|---|
|
||||
| Bridge protocol (message shapes) | `packages/webview-bridge/src/types.ts` | Canonical — native must match |
|
||||
| Storage adapter expectations | `packages/webview-bridge/src/adapters/storage.ts:15-18` | `get()` returns `{ value: string \| null }` |
|
||||
| Crypto adapter expectations | `packages/webview-bridge/src/adapters/crypto.ts` | Defines request params and response shapes |
|
||||
| CryptoHandler (Android ref) | `packages/native-shell-android/.../handlers/CryptoHandler.kt` | Reference for default `AndroidKeystoreCryptoProvider` — uses AndroidKeyStore, secp256r1, SHA256withECDSA |
|
||||
| SecureStorageHandler (Android ref) | `packages/native-shell-android/.../handlers/SecureStorageHandler.kt` | Reference for provider-delegated handler — wraps `get()` in `{ value: ... }` |
|
||||
| WebChromeClient | `packages/native-shell-android/.../webview/AndroidWebViewHost.kt:109-173` | Port permission + file upload handling |
|
||||
| iOS provider registry | `packages/kmp-sdk/.../iosMain/.../providers/SdkProviderRegistry.kt` | Current iOS-only registry — move to commonMain |
|
||||
| iOS CryptoBridgeHandler | `packages/kmp-sdk/.../iosMain/.../handlers/CryptoBridgeHandler.kt` | Provider-delegated crypto — reuse pattern for both platforms |
|
||||
|
||||
## Bridge Domain Contract
|
||||
|
||||
Only 3 domains are registered by the scoped KMP:
|
||||
|
||||
### `secureStorage`
|
||||
|
||||
| Method | Params | Response |
|
||||
|---|---|---|
|
||||
| `get` | `{ key: string }` | `{ value: string \| null }` |
|
||||
| `set` | `{ key: string, value: string }` | `null` |
|
||||
| `remove` | `{ key: string }` | `null` |
|
||||
|
||||
### `crypto`
|
||||
|
||||
| Method | Params | Response |
|
||||
|---|---|---|
|
||||
| `generateKey` | `{ keyRef: string }` | `{ keyRef: string, success: true }` |
|
||||
| `getPublicKey` | `{ keyRef: string }` | `{ publicKey: string }` (base64) |
|
||||
| `sign` | `{ data: string, keyRef: string }` (data is base64) | `{ signature: string }` (base64) |
|
||||
|
||||
### `lifecycle`
|
||||
|
||||
| Method | Params | Response |
|
||||
|---|---|---|
|
||||
| `ready` | `{}` | `null` (no-op) |
|
||||
| `dismiss` | `{ reason?: string }` | `null` (finishes Activity / dismisses VC) |
|
||||
| `setResult` | `{ success: bool, userId?, verificationId?, error? }` | `null` (forwards to host, then finishes) |
|
||||
|
||||
Any other domain request returns a `DOMAIN_NOT_FOUND` error response.
|
||||
|
||||
## Related Specs
|
||||
|
||||
| Spec | Relationship |
|
||||
|---|---|
|
||||
| [SDK Overview](../../OVERVIEW.md) | Parent architecture |
|
||||
| [Native Shells Lite](../native-shells-lite/SPEC.md) | Sibling — serves non-KMP consumers |
|
||||
| [Paused Native Shells (KMP)](../../paused/native-shells/SPEC.md) | Historical KMP work — validated foundation |
|
||||
| [Build Pipeline](../build-pipeline/SPEC.md) | Downstream — bundles webview-app into native assets |
|
||||
@@ -0,0 +1,758 @@
|
||||
## Scope KMP Android to 3-Domain Parity with Provider Delegation
|
||||
|
||||
> Last updated: 2026-04-01
|
||||
> Status: Ready
|
||||
|
||||
- Workstream: kmp-revival
|
||||
- Backlog IDs: KR-01
|
||||
- Owner: TBD
|
||||
- Branch: TBD
|
||||
- PR: TBD
|
||||
|
||||
### Why
|
||||
|
||||
The KMP SDK Android target registers 5 handlers (NFC, Camera, Biometric, SecureStorage, Lifecycle) but is missing the `crypto` handler that native-shells-lite provides. The Android SecureStorage handler hardcodes `EncryptedSharedPreferences` instead of delegating to a consumer-provided provider (the iOS target already delegates via `SdkProviderRegistry`). There are also gaps in WebView capabilities (no WebChromeClient, no query params), an incorrect SecureStorage response shape, and no bridge protocol version validation.
|
||||
|
||||
You are closing these gaps by:
|
||||
1. Moving provider interfaces to `commonMain` so both platforms share the same contract
|
||||
2. Making Android handlers delegate to providers (matching iOS pattern)
|
||||
3. Shipping default Android provider implementations consumers can use or replace
|
||||
4. Bringing WebView host to parity with native-shell-android
|
||||
|
||||
### Scope
|
||||
|
||||
- `packages/kmp-sdk/` only (primarily Android target + commonMain provider interfaces)
|
||||
- 3 bridge domains: `secureStorage`, `crypto`, `lifecycle`
|
||||
- Provider delegation: move interfaces to commonMain, add default Android implementations
|
||||
- WebView host upgrades (WebChromeClient, query params)
|
||||
- Bridge alignment (response shapes, protocol version)
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- iOS target handler changes (see KR-02) — but iOS will benefit from the commonMain interface move
|
||||
- `packages/native-shell-android/` — do not modify
|
||||
- `packages/webview-bridge/` — bridge protocol unchanged
|
||||
- `packages/webview-app/` — WebView code unchanged
|
||||
- NFC, Camera, Biometric handler code — retain files but do not register
|
||||
- Publishing (see KR-03)
|
||||
|
||||
### Implementation Steps
|
||||
|
||||
#### 1. Move provider interfaces to commonMain
|
||||
|
||||
Currently `SecureStorageProvider` and `CryptoProvider` live in `iosMain/kotlin/xyz/self/sdk/providers/`. Move them to `commonMain` so both platforms share the same contract.
|
||||
|
||||
**Move:**
|
||||
- `packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/providers/SecureStorageProvider.kt` → `packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/providers/SecureStorageProvider.kt`
|
||||
- `packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/providers/CryptoProvider.kt` → `packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/providers/CryptoProvider.kt`
|
||||
|
||||
The interfaces remain identical — same package (`xyz.self.sdk.providers`), same methods. The iOS handlers that reference them will compile without changes since `commonMain` is visible to `iosMain`.
|
||||
|
||||
**Current `SecureStorageProvider` interface (iosMain):**
|
||||
```kotlin
|
||||
interface SecureStorageProvider {
|
||||
fun get(key: String): String?
|
||||
fun set(key: String, value: String)
|
||||
fun remove(key: String)
|
||||
fun clear()
|
||||
}
|
||||
```
|
||||
|
||||
**Current `CryptoProvider` interface (iosMain):**
|
||||
```kotlin
|
||||
interface CryptoProvider {
|
||||
fun generateKey(keyRef: String)
|
||||
fun getPublicKey(keyRef: String): String?
|
||||
fun sign(keyRef: String, data: String): String?
|
||||
fun deleteKey(keyRef: String)
|
||||
}
|
||||
```
|
||||
|
||||
No changes to the interfaces themselves — just the source set location.
|
||||
|
||||
#### 2. Move SdkProviderRegistry to commonMain
|
||||
|
||||
**Current file:** `packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/providers/SdkProviderRegistry.kt`
|
||||
|
||||
**Current state:** iOS-only, requires all 8 providers (biometric, secureStorage, haptic, crypto, documents, nfc, cameraMrz, webView) to be non-null for `isConfigured()` to return true.
|
||||
|
||||
**Move to:** `packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/providers/SdkProviderRegistry.kt`
|
||||
|
||||
**Rewrite for 3-domain scope:**
|
||||
```kotlin
|
||||
package xyz.self.sdk.providers
|
||||
|
||||
object SdkProviderRegistry {
|
||||
var secureStorage: SecureStorageProvider? = null
|
||||
var crypto: CryptoProvider? = null
|
||||
|
||||
// Optional providers — retained for future handler registration
|
||||
var biometric: BiometricProvider? = null
|
||||
var haptic: HapticProvider? = null
|
||||
var documents: DocumentsProvider? = null
|
||||
var nfc: NfcProvider? = null
|
||||
var cameraMrz: CameraMrzProvider? = null
|
||||
var webView: WebViewProvider? = null
|
||||
|
||||
/**
|
||||
* Returns true if the required 3-domain providers are configured.
|
||||
* Only secureStorage and crypto are required. WebView provider is
|
||||
* platform-specific (Android uses Activity-hosted WebView, iOS delegates).
|
||||
*/
|
||||
fun isConfigured(): Boolean =
|
||||
secureStorage != null && crypto != null
|
||||
|
||||
fun reset() {
|
||||
secureStorage = null
|
||||
crypto = null
|
||||
biometric = null
|
||||
haptic = null
|
||||
documents = null
|
||||
nfc = null
|
||||
cameraMrz = null
|
||||
webView = null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key changes:**
|
||||
- `isConfigured()` now checks only the 2 required providers (secureStorage, crypto) instead of all 8
|
||||
- Added `reset()` for clean teardown between sessions
|
||||
- Optional providers retained for future use but not required
|
||||
- `webView` is kept optional — Android hosts WebView via Activity, iOS delegates to consumer
|
||||
|
||||
**Note:** The other provider interfaces (`BiometricProvider`, `HapticProvider`, etc.) that live in `iosMain` should remain there for now. Only move what's needed for the 3-domain scope. If they cause compile errors after moving `SdkProviderRegistry`, create empty `expect`/`actual` stubs or use nullable types (they're already nullable).
|
||||
|
||||
#### 3. Create default Android provider implementations
|
||||
|
||||
**New file:** `packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/providers/EncryptedSharedPreferencesProvider.kt`
|
||||
|
||||
Extract the storage logic from the current `SecureStorageBridgeHandler` into a standalone provider:
|
||||
|
||||
```kotlin
|
||||
package xyz.self.sdk.providers
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
|
||||
/**
|
||||
* Default Android SecureStorageProvider using EncryptedSharedPreferences
|
||||
* backed by Android Keystore. Consumers can replace this with their own
|
||||
* implementation via SdkProviderRegistry.secureStorage.
|
||||
*/
|
||||
class EncryptedSharedPreferencesProvider(context: Context) : SecureStorageProvider {
|
||||
private val prefs: SharedPreferences
|
||||
|
||||
init {
|
||||
val masterKey = MasterKey.Builder(context)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build()
|
||||
prefs = EncryptedSharedPreferences.create(
|
||||
context,
|
||||
"self_sdk_secure_prefs",
|
||||
masterKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||
)
|
||||
}
|
||||
|
||||
override fun get(key: String): String? = prefs.getString(key, null)
|
||||
override fun set(key: String, value: String) { prefs.edit().putString(key, value).apply() }
|
||||
override fun remove(key: String) { prefs.edit().remove(key).apply() }
|
||||
override fun clear() { prefs.edit().clear().apply() }
|
||||
}
|
||||
```
|
||||
|
||||
**New file:** `packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/providers/AndroidKeystoreCryptoProvider.kt`
|
||||
|
||||
Extract crypto logic from `native-shell-android/.../handlers/CryptoHandler.kt`:
|
||||
|
||||
```kotlin
|
||||
package xyz.self.sdk.providers
|
||||
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.util.Base64
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
import java.security.Signature
|
||||
import java.security.spec.ECGenParameterSpec
|
||||
|
||||
/**
|
||||
* Default Android CryptoProvider using AndroidKeyStore with secp256r1/SHA256withECDSA.
|
||||
* Consumers can replace this with their own implementation via SdkProviderRegistry.crypto.
|
||||
*/
|
||||
class AndroidKeystoreCryptoProvider : CryptoProvider {
|
||||
private val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
||||
|
||||
override fun generateKey(keyRef: String) {
|
||||
if (keyStore.containsAlias(keyRef)) {
|
||||
keyStore.deleteEntry(keyRef)
|
||||
}
|
||||
val spec = KeyGenParameterSpec.Builder(
|
||||
keyRef,
|
||||
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY,
|
||||
)
|
||||
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
|
||||
.setDigests(KeyProperties.DIGEST_SHA256)
|
||||
.build()
|
||||
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore").apply {
|
||||
initialize(spec)
|
||||
generateKeyPair()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPublicKey(keyRef: String): String? {
|
||||
val cert = keyStore.getCertificate(keyRef) ?: return null
|
||||
return Base64.encodeToString(cert.publicKey.encoded, Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
override fun sign(keyRef: String, data: String): String? {
|
||||
val privateKey = keyStore.getKey(keyRef, null) ?: return null
|
||||
val dataBytes = Base64.decode(data, Base64.DEFAULT)
|
||||
val signature = Signature.getInstance("SHA256withECDSA").apply {
|
||||
initSign(privateKey as java.security.PrivateKey)
|
||||
update(dataBytes)
|
||||
}.sign()
|
||||
return Base64.encodeToString(signature, Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
override fun deleteKey(keyRef: String) {
|
||||
if (keyStore.containsAlias(keyRef)) {
|
||||
keyStore.deleteEntry(keyRef)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Rewrite SecureStorageBridgeHandler to delegate to provider
|
||||
|
||||
**File:** `packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/handlers/SecureStorageBridgeHandler.kt`
|
||||
|
||||
Replace the current handler (which hardcodes `EncryptedSharedPreferences`) with a provider-delegated version. This also fixes the `get()` response shape bug (currently returns bare primitives, needs `{ value: ... }` wrapping).
|
||||
|
||||
```kotlin
|
||||
package xyz.self.sdk.handlers
|
||||
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import xyz.self.sdk.bridge.BridgeDomain
|
||||
import xyz.self.sdk.bridge.BridgeHandler
|
||||
import xyz.self.sdk.bridge.BridgeHandlerException
|
||||
import xyz.self.sdk.providers.SdkProviderRegistry
|
||||
|
||||
class SecureStorageBridgeHandler : BridgeHandler {
|
||||
override val domain = BridgeDomain.SECURE_STORAGE
|
||||
|
||||
override suspend fun handle(
|
||||
method: String,
|
||||
params: Map<String, JsonElement>,
|
||||
): JsonElement? = when (method) {
|
||||
"get" -> get(params)
|
||||
"set" -> set(params)
|
||||
"remove" -> remove(params)
|
||||
"clear" -> clear()
|
||||
else -> throw BridgeHandlerException("METHOD_NOT_FOUND", "Unknown secureStorage method: $method")
|
||||
}
|
||||
|
||||
private fun get(params: Map<String, JsonElement>): JsonElement {
|
||||
val provider = SdkProviderRegistry.secureStorage
|
||||
?: throw BridgeHandlerException("NOT_CONFIGURED", "SecureStorage provider not configured")
|
||||
val key = params["key"]?.jsonPrimitive?.content
|
||||
?: throw BridgeHandlerException("MISSING_KEY", "Key parameter required")
|
||||
val value = provider.get(key)
|
||||
return buildJsonObject {
|
||||
put("value", if (value != null) JsonPrimitive(value) else JsonNull)
|
||||
}
|
||||
}
|
||||
|
||||
private fun set(params: Map<String, JsonElement>): JsonElement? {
|
||||
val provider = SdkProviderRegistry.secureStorage
|
||||
?: throw BridgeHandlerException("NOT_CONFIGURED", "SecureStorage provider not configured")
|
||||
val key = params["key"]?.jsonPrimitive?.content
|
||||
?: throw BridgeHandlerException("MISSING_KEY", "Key parameter required")
|
||||
val value = params["value"]?.jsonPrimitive?.content
|
||||
?: throw BridgeHandlerException("MISSING_VALUE", "Value parameter required")
|
||||
provider.set(key, value)
|
||||
return null
|
||||
}
|
||||
|
||||
private fun remove(params: Map<String, JsonElement>): JsonElement? {
|
||||
val provider = SdkProviderRegistry.secureStorage
|
||||
?: throw BridgeHandlerException("NOT_CONFIGURED", "SecureStorage provider not configured")
|
||||
val key = params["key"]?.jsonPrimitive?.content
|
||||
?: throw BridgeHandlerException("MISSING_KEY", "Key parameter required")
|
||||
provider.remove(key)
|
||||
return null
|
||||
}
|
||||
|
||||
private fun clear(): JsonElement? {
|
||||
val provider = SdkProviderRegistry.secureStorage
|
||||
?: throw BridgeHandlerException("NOT_CONFIGURED", "SecureStorage provider not configured")
|
||||
provider.clear()
|
||||
return null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key changes from current:**
|
||||
- No longer takes `Context` parameter (no direct EncryptedSharedPreferences)
|
||||
- Delegates to `SdkProviderRegistry.secureStorage`
|
||||
- `get()` returns `buildJsonObject { put("value", ...) }` instead of bare `JsonPrimitive`/`JsonNull`
|
||||
- Throws `NOT_CONFIGURED` if provider not set (fail-closed)
|
||||
|
||||
#### 5. Add CryptoBridgeHandler for Android
|
||||
|
||||
**New file:** `packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/handlers/CryptoBridgeHandler.kt`
|
||||
|
||||
The `androidMain` handlers directory currently has NFC, Camera, Biometric, SecureStorage, and Lifecycle — but no crypto handler. The iOS target has `CryptoBridgeHandler.kt` in `iosMain/` which delegates to `SdkProviderRegistry.crypto`. Create the same for Android.
|
||||
|
||||
Since both platforms now share `SdkProviderRegistry` in `commonMain`, the Android CryptoBridgeHandler can be **identical** to the iOS one. Consider moving it to `commonMain` as a shared handler, or keep platform-specific copies if the error handling differs.
|
||||
|
||||
**Recommended: move to commonMain** since the handler is purely provider-delegated with no platform imports:
|
||||
|
||||
**New file:** `packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/handlers/CryptoBridgeHandler.kt`
|
||||
|
||||
```kotlin
|
||||
package xyz.self.sdk.handlers
|
||||
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import xyz.self.sdk.bridge.BridgeDomain
|
||||
import xyz.self.sdk.bridge.BridgeHandler
|
||||
import xyz.self.sdk.bridge.BridgeHandlerException
|
||||
import xyz.self.sdk.providers.SdkProviderRegistry
|
||||
|
||||
class CryptoBridgeHandler : BridgeHandler {
|
||||
override val domain = BridgeDomain.CRYPTO
|
||||
|
||||
override suspend fun handle(
|
||||
method: String,
|
||||
params: Map<String, JsonElement>,
|
||||
): JsonElement? = when (method) {
|
||||
"generateKey" -> generateKey(params)
|
||||
"getPublicKey" -> getPublicKey(params)
|
||||
"sign" -> sign(params)
|
||||
"deleteKey" -> deleteKey(params)
|
||||
else -> throw BridgeHandlerException("METHOD_NOT_FOUND", "Unknown crypto method: $method")
|
||||
}
|
||||
|
||||
private fun generateKey(params: Map<String, JsonElement>): JsonElement {
|
||||
val provider = SdkProviderRegistry.crypto
|
||||
?: throw BridgeHandlerException("NOT_CONFIGURED", "Crypto provider not configured")
|
||||
val keyRef = params["keyRef"]?.jsonPrimitive?.content
|
||||
?: throw BridgeHandlerException("MISSING_PARAM", "keyRef parameter required")
|
||||
provider.generateKey(keyRef)
|
||||
return buildJsonObject {
|
||||
put("keyRef", JsonPrimitive(keyRef))
|
||||
put("success", JsonPrimitive(true))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPublicKey(params: Map<String, JsonElement>): JsonElement {
|
||||
val provider = SdkProviderRegistry.crypto
|
||||
?: throw BridgeHandlerException("NOT_CONFIGURED", "Crypto provider not configured")
|
||||
val keyRef = params["keyRef"]?.jsonPrimitive?.content
|
||||
?: throw BridgeHandlerException("MISSING_PARAM", "keyRef parameter required")
|
||||
val publicKey = provider.getPublicKey(keyRef)
|
||||
?: throw BridgeHandlerException("KEY_NOT_FOUND", "Key not found: $keyRef")
|
||||
return buildJsonObject {
|
||||
put("publicKey", JsonPrimitive(publicKey))
|
||||
}
|
||||
}
|
||||
|
||||
private fun sign(params: Map<String, JsonElement>): JsonElement {
|
||||
val provider = SdkProviderRegistry.crypto
|
||||
?: throw BridgeHandlerException("NOT_CONFIGURED", "Crypto provider not configured")
|
||||
val keyRef = params["keyRef"]?.jsonPrimitive?.content
|
||||
?: throw BridgeHandlerException("MISSING_PARAM", "keyRef parameter required")
|
||||
val data = params["data"]?.jsonPrimitive?.content
|
||||
?: throw BridgeHandlerException("MISSING_PARAM", "data parameter required")
|
||||
val signature = provider.sign(keyRef, data)
|
||||
?: throw BridgeHandlerException("SIGN_FAILED", "Signing failed for key: $keyRef")
|
||||
return buildJsonObject {
|
||||
put("signature", JsonPrimitive(signature))
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteKey(params: Map<String, JsonElement>): JsonElement? {
|
||||
val provider = SdkProviderRegistry.crypto
|
||||
?: throw BridgeHandlerException("NOT_CONFIGURED", "Crypto provider not configured")
|
||||
val keyRef = params["keyRef"]?.jsonPrimitive?.content
|
||||
?: throw BridgeHandlerException("MISSING_PARAM", "keyRef parameter required")
|
||||
provider.deleteKey(keyRef)
|
||||
return null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If moved to `commonMain`, **delete** the existing `iosMain/.../handlers/CryptoBridgeHandler.kt` to avoid duplicate class definitions.
|
||||
|
||||
Response shapes match `webview-bridge/src/adapters/crypto.ts`:
|
||||
- `generateKey` → `{ keyRef: string, success: true }`
|
||||
- `getPublicKey` → `{ publicKey: string }` (base64)
|
||||
- `sign` → `{ signature: string }` (base64)
|
||||
|
||||
#### 6. Scope handler registration to 3 domains
|
||||
|
||||
**File:** `packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/SelfVerificationActivity.kt`
|
||||
|
||||
**Current `registerHandlers()` (lines 88-103):**
|
||||
```kotlin
|
||||
private fun registerHandlers() {
|
||||
router.register(NfcBridgeHandler(this, router))
|
||||
router.register(CameraMrzBridgeHandler(this))
|
||||
router.register(BiometricBridgeHandler(this))
|
||||
router.register(SecureStorageBridgeHandler(this))
|
||||
router.register(LifecycleBridgeHandler(this))
|
||||
}
|
||||
```
|
||||
|
||||
**Change to:**
|
||||
```kotlin
|
||||
private fun registerHandlers() {
|
||||
router.register(SecureStorageBridgeHandler()) // no Context — delegates to provider
|
||||
router.register(CryptoBridgeHandler()) // delegates to provider
|
||||
router.register(LifecycleBridgeHandler(this))
|
||||
}
|
||||
```
|
||||
|
||||
**Also:**
|
||||
- Remove `requiredPermissions` array, `permissionLauncher`, and the permission-check block in `onCreate()` (lines 29-56). The 3-domain scope does not need CAMERA or NFC permissions at startup. The WebChromeClient (step 8) handles camera permissions on-demand.
|
||||
- Simplify `onCreate()` to call `initVerificationFlow()` directly.
|
||||
- Add provider initialization before handler registration:
|
||||
|
||||
```kotlin
|
||||
private fun initVerificationFlow() {
|
||||
// Register default providers if consumer hasn't set custom ones
|
||||
if (SdkProviderRegistry.secureStorage == null) {
|
||||
SdkProviderRegistry.secureStorage = EncryptedSharedPreferencesProvider(this)
|
||||
}
|
||||
if (SdkProviderRegistry.crypto == null) {
|
||||
SdkProviderRegistry.crypto = AndroidKeystoreCryptoProvider()
|
||||
}
|
||||
|
||||
// ... router setup, handler registration, WebView creation
|
||||
}
|
||||
```
|
||||
|
||||
This gives consumers the option to set their own providers before launching the Activity (via `SdkProviderRegistry.secureStorage = MyProvider()`), or get sensible defaults.
|
||||
|
||||
- Remove unused imports: `BiometricBridgeHandler`, `CameraMrzBridgeHandler`, `NfcBridgeHandler`, `Manifest`, `PackageManager`, `ActivityResultContracts`, `ContextCompat`.
|
||||
- Add imports: `CryptoBridgeHandler`, `SdkProviderRegistry`, `EncryptedSharedPreferencesProvider`, `AndroidKeystoreCryptoProvider`.
|
||||
|
||||
#### 7. Add query param support to WebView URL loading
|
||||
|
||||
**File:** `packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/AndroidWebViewHost.kt`
|
||||
|
||||
**Current `createWebView()` signature (line 37):** `fun createWebView(): WebView`
|
||||
|
||||
**Change to:** `fun createWebView(queryParams: String = ""): WebView`
|
||||
|
||||
**Update loadUrl calls (lines 129-139):**
|
||||
```kotlin
|
||||
// Before
|
||||
webView.loadUrl("https://appassets.androidplatform.net/index.html")
|
||||
|
||||
// After
|
||||
val baseUrl = "https://appassets.androidplatform.net/index.html"
|
||||
val url = if (queryParams.isNotEmpty()) "$baseUrl?$queryParams" else baseUrl
|
||||
webView.loadUrl(url)
|
||||
```
|
||||
|
||||
Apply the same pattern to the debug URL (`http://127.0.0.1:5173`).
|
||||
|
||||
**Update caller in SelfVerificationActivity:** Build query params from intent extras (or `VerificationRequest` if available) and pass to `createWebView(queryParams)`. Reference `packages/native-shell-android/.../SelfVerificationActivity.kt:64-84` for the query string builder pattern using `buildString { }` with `Uri.encode()`.
|
||||
|
||||
The native-shell-android extracts 14 intent extras and encodes them. KMP currently only extracts `EXTRA_DEBUG_MODE`, `EXTRA_VERIFICATION_REQUEST`, and `EXTRA_CONFIG`. You need to either:
|
||||
- Parse `EXTRA_VERIFICATION_REQUEST` JSON and build query params from it, or
|
||||
- Add the same 14 intent extras as native-shell-android
|
||||
|
||||
The first approach is cleaner since KMP already has structured `VerificationRequest` types.
|
||||
|
||||
#### 8. Add WebChromeClient to AndroidWebViewHost
|
||||
|
||||
**File:** `packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/AndroidWebViewHost.kt`
|
||||
|
||||
**Current (line 122):** After the `webViewClient` block, there is no `webChromeClient`.
|
||||
|
||||
**Port from:** `packages/native-shell-android/src/main/kotlin/xyz/self/sdk/webview/AndroidWebViewHost.kt:109-173`
|
||||
|
||||
Add after the existing `webViewClient` block (after line 122):
|
||||
|
||||
```kotlin
|
||||
webChromeClient = object : WebChromeClient() {
|
||||
override fun onPermissionRequest(request: PermissionRequest?) {
|
||||
request ?: return
|
||||
val origin = request.origin?.toString() ?: ""
|
||||
val isTrusted = origin.startsWith("https://appassets.androidplatform.net") ||
|
||||
(isDebugMode && origin.startsWith("http://127.0.0.1"))
|
||||
if (!isTrusted) {
|
||||
request.deny()
|
||||
return
|
||||
}
|
||||
|
||||
val activity = context as? Activity ?: run {
|
||||
request.deny()
|
||||
return
|
||||
}
|
||||
|
||||
val neededPermissions = mutableListOf<String>()
|
||||
if (request.resources.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) {
|
||||
neededPermissions.add(Manifest.permission.CAMERA)
|
||||
}
|
||||
if (request.resources.contains(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) {
|
||||
neededPermissions.add(Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
|
||||
val missingPermissions = neededPermissions.filter {
|
||||
ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
if (missingPermissions.isNotEmpty()) {
|
||||
pendingPermissionRequest = request
|
||||
ActivityCompat.requestPermissions(
|
||||
activity, missingPermissions.toTypedArray(), CAMERA_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
request.grant(request.resources)
|
||||
}
|
||||
|
||||
override fun onShowFileChooser(
|
||||
webView: WebView?,
|
||||
filePathCallback: ValueCallback<Array<Uri>>?,
|
||||
fileChooserParams: FileChooserParams?,
|
||||
): Boolean {
|
||||
fileUploadCallback?.onReceiveValue(null)
|
||||
fileUploadCallback = filePathCallback
|
||||
val intent = fileChooserParams?.createIntent() ?: return false
|
||||
val activity = context as? Activity ?: run {
|
||||
fileUploadCallback = null
|
||||
return false
|
||||
}
|
||||
try {
|
||||
activity.startActivityForResult(intent, FILE_CHOOSER_REQUEST_CODE)
|
||||
} catch (e: Exception) {
|
||||
fileUploadCallback = null
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add class-level fields:
|
||||
```kotlin
|
||||
var pendingPermissionRequest: PermissionRequest? = null
|
||||
var fileUploadCallback: ValueCallback<Array<Uri>>? = null
|
||||
```
|
||||
|
||||
Add companion object constants:
|
||||
```kotlin
|
||||
companion object {
|
||||
const val FILE_CHOOSER_REQUEST_CODE = 1001
|
||||
const val CAMERA_PERMISSION_REQUEST_CODE = 1002
|
||||
}
|
||||
```
|
||||
|
||||
**Also add permission result handling** to `SelfVerificationActivity`:
|
||||
```kotlin
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode == AndroidWebViewHost.CAMERA_PERMISSION_REQUEST_CODE) {
|
||||
val pending = webViewHost.pendingPermissionRequest ?: return
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
pending.grant(pending.resources)
|
||||
} else {
|
||||
pending.deny()
|
||||
}
|
||||
webViewHost.pendingPermissionRequest = null
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use Activity Result API")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == AndroidWebViewHost.FILE_CHOOSER_REQUEST_CODE) {
|
||||
val results = if (resultCode == RESULT_OK && data != null) {
|
||||
WebChromeClient.FileChooserParams.parseResult(resultCode, data)
|
||||
} else null
|
||||
webViewHost.fileUploadCallback?.onReceiveValue(results)
|
||||
webViewHost.fileUploadCallback = null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 9. Add protocol version validation to MessageRouter
|
||||
|
||||
**File:** `packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/bridge/MessageRouter.kt`
|
||||
|
||||
**Current (lines 26-32):** After parsing the BridgeRequest, handler lookup proceeds immediately with no version check.
|
||||
|
||||
**Add after parsing (line 32, before handler lookup):**
|
||||
```kotlin
|
||||
val request = json.decodeFromString<BridgeRequest>(rawJson)
|
||||
|
||||
// Add this check
|
||||
if (request.version != BRIDGE_PROTOCOL_VERSION) {
|
||||
sendResponse(BridgeResponse(
|
||||
id = generateUuid(),
|
||||
domain = request.domain,
|
||||
requestId = request.id,
|
||||
success = false,
|
||||
error = BridgeError("UNSUPPORTED_VERSION", "Expected protocol version $BRIDGE_PROTOCOL_VERSION, got ${request.version}"),
|
||||
))
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
Add companion object constant (matching `webview-bridge/src/types.ts:152`):
|
||||
```kotlin
|
||||
companion object {
|
||||
const val BRIDGE_PROTOCOL_VERSION = 1
|
||||
|
||||
fun escapeForJs(jsonStr: String): String { ... } // existing
|
||||
}
|
||||
```
|
||||
|
||||
#### 10. Trim unused dependencies from build.gradle.kts
|
||||
|
||||
**File:** `packages/kmp-sdk/shared/build.gradle.kts`
|
||||
|
||||
**Remove or comment out these androidMain dependencies:**
|
||||
```kotlin
|
||||
// NFC/Passport — not needed for 3-domain scope
|
||||
// implementation("org.jmrtd:jmrtd:0.8.1")
|
||||
// implementation("net.sf.scuba:scuba-sc-android:0.0.18")
|
||||
// implementation("org.bouncycastle:bcprov-jdk18on:1.78.1")
|
||||
// implementation("commons-io:commons-io:2.14.0")
|
||||
|
||||
// Biometrics — not needed for 3-domain scope
|
||||
// implementation("androidx.biometric:biometric:1.2.0-alpha05")
|
||||
|
||||
// Camera/MRZ — not needed for 3-domain scope
|
||||
// implementation("com.google.mlkit:text-recognition:16.0.1")
|
||||
// implementation("androidx.camera:camera-camera2:1.4.1")
|
||||
// implementation("androidx.camera:camera-lifecycle:1.4.1")
|
||||
// implementation("androidx.camera:camera-view:1.4.1")
|
||||
```
|
||||
|
||||
**Keep:**
|
||||
- `androidx.webkit:webkit` — WebView
|
||||
- `androidx.security:security-crypto` — Default EncryptedSharedPreferencesProvider
|
||||
- `androidx.appcompat:appcompat` — Activity
|
||||
- `androidx.activity:activity-ktx` — Activity Result API
|
||||
- `androidx.lifecycle:lifecycle-runtime-ktx` — Lifecycle
|
||||
- `kotlinx.serialization` — Bridge JSON
|
||||
- `kotlinx.coroutines` — Async handlers
|
||||
|
||||
### Files Created
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `shared/src/commonMain/.../providers/SecureStorageProvider.kt` | Moved from iosMain — shared interface |
|
||||
| `shared/src/commonMain/.../providers/CryptoProvider.kt` | Moved from iosMain — shared interface |
|
||||
| `shared/src/commonMain/.../providers/SdkProviderRegistry.kt` | Moved from iosMain — unified registry, 3-domain `isConfigured()` |
|
||||
| `shared/src/commonMain/.../handlers/CryptoBridgeHandler.kt` | Provider-delegated crypto handler (replaces iOS-only version) |
|
||||
| `shared/src/androidMain/.../providers/EncryptedSharedPreferencesProvider.kt` | Default Android SecureStorageProvider |
|
||||
| `shared/src/androidMain/.../providers/AndroidKeystoreCryptoProvider.kt` | Default Android CryptoProvider |
|
||||
|
||||
### Files Modified
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `shared/src/androidMain/.../handlers/SecureStorageBridgeHandler.kt` | Rewrite: delegate to provider, fix `get()` response shape |
|
||||
| `shared/src/androidMain/.../webview/SelfVerificationActivity.kt` | Register 3 handlers, default provider init, remove permission requests, add query params, add permission/file callbacks |
|
||||
| `shared/src/androidMain/.../webview/AndroidWebViewHost.kt` | Add WebChromeClient, query params, permission/file fields |
|
||||
| `shared/src/commonMain/.../bridge/MessageRouter.kt` | Add protocol version validation |
|
||||
| `shared/build.gradle.kts` | Remove NFC/camera/biometric dependencies |
|
||||
|
||||
### Files Deleted
|
||||
|
||||
| File | Reason |
|
||||
|------|--------|
|
||||
| `shared/src/iosMain/.../providers/SecureStorageProvider.kt` | Moved to commonMain |
|
||||
| `shared/src/iosMain/.../providers/CryptoProvider.kt` | Moved to commonMain |
|
||||
| `shared/src/iosMain/.../providers/SdkProviderRegistry.kt` | Moved to commonMain |
|
||||
| `shared/src/iosMain/.../handlers/CryptoBridgeHandler.kt` | Replaced by commonMain version |
|
||||
|
||||
### Files NOT Modified
|
||||
|
||||
- `packages/native-shell-android/` — sibling, serves different consumers
|
||||
- `packages/native-shell-ios/` — sibling, serves different consumers
|
||||
- `packages/webview-bridge/` — bridge protocol unchanged
|
||||
- `packages/webview-app/` — WebView code unchanged
|
||||
- `packages/mobile-sdk-alpha/` — SDK core unchanged
|
||||
- `app/` — RN app unaffected
|
||||
- Existing NFC/Camera/Biometric handler source files in androidMain — retained, just not registered
|
||||
- `packages/self-sdk-swift/` — iOS providers unchanged (KR-02 scope)
|
||||
|
||||
### Preconditions
|
||||
|
||||
- `packages/webview-app/` builds and `dist/` output exists (for asset bundling)
|
||||
- `packages/webview-bridge/` bridge protocol types are stable
|
||||
|
||||
### Validation
|
||||
|
||||
```bash
|
||||
# Build KMP Android
|
||||
cd packages/kmp-sdk && ./gradlew :shared:assembleDebug
|
||||
|
||||
# Run existing tests (must not regress)
|
||||
cd packages/kmp-sdk && ./gradlew :shared:jvmTest
|
||||
|
||||
# Verify AAR artifact
|
||||
cd packages/kmp-sdk && ./gradlew :shared:assembleRelease
|
||||
ls -la shared/build/outputs/aar/shared-release.aar
|
||||
|
||||
# Verify provider delegation
|
||||
# Add unit test: SdkProviderRegistry.isConfigured() returns false when providers are null
|
||||
# Add unit test: SdkProviderRegistry.isConfigured() returns true with secureStorage + crypto set
|
||||
# Add unit test: SecureStorageBridgeHandler throws NOT_CONFIGURED when provider is null
|
||||
# Add unit test: CryptoBridgeHandler throws NOT_CONFIGURED when provider is null
|
||||
|
||||
# Verify response shapes
|
||||
# Add unit test: SecureStorageBridgeHandler.get() returns JsonObject with "value" key
|
||||
# Add unit test: CryptoBridgeHandler.generateKey returns { keyRef, success: true }
|
||||
# Add unit test: CryptoBridgeHandler.getPublicKey returns { publicKey } (base64 string)
|
||||
# Add unit test: CryptoBridgeHandler.sign returns { signature } (base64 string)
|
||||
|
||||
# Verify version validation
|
||||
# Add unit test: MessageRouter rejects requests with wrong protocol version
|
||||
```
|
||||
|
||||
### Definition of Done
|
||||
|
||||
- [ ] `SecureStorageProvider` and `CryptoProvider` interfaces live in `commonMain`
|
||||
- [ ] `SdkProviderRegistry` lives in `commonMain` with 3-domain `isConfigured()` check
|
||||
- [ ] `EncryptedSharedPreferencesProvider` ships as default Android SecureStorageProvider
|
||||
- [ ] `AndroidKeystoreCryptoProvider` ships as default Android CryptoProvider
|
||||
- [ ] `SecureStorageBridgeHandler` delegates to provider, `get()` returns `{ value: string | null }`
|
||||
- [ ] `CryptoBridgeHandler` lives in `commonMain`, delegates to provider, handles `generateKey`, `getPublicKey`, `sign`, `deleteKey`
|
||||
- [ ] `SelfVerificationActivity` registers only 3 handlers, initializes default providers
|
||||
- [ ] Camera/NFC permission requests removed from Activity startup
|
||||
- [ ] `MessageRouter` validates protocol version and rejects mismatches
|
||||
- [ ] `AndroidWebViewHost` has `WebChromeClient` with permission + file upload handling
|
||||
- [ ] `AndroidWebViewHost.createWebView()` supports query params
|
||||
- [ ] Activity handles `onRequestPermissionsResult` and `onActivityResult` for WebChromeClient
|
||||
- [ ] NFC/camera/biometric dependencies removed from `build.gradle.kts`
|
||||
- [ ] All existing KMP tests pass (`./gradlew :shared:jvmTest`)
|
||||
- [ ] Android AAR builds cleanly (`./gradlew :shared:assembleRelease`)
|
||||
|
||||
### Estimated PR Size
|
||||
|
||||
~600–900 LOC changed. Within the 1k–3k target.
|
||||
|
||||
### Status Log
|
||||
|
||||
- 2026-03-31: Plan created.
|
||||
- 2026-04-01: Rewritten with provider delegation approach. Provider interfaces move to commonMain, Android gets default implementations (EncryptedSharedPreferencesProvider, AndroidKeystoreCryptoProvider). CryptoBridgeHandler shared across platforms. Verified all file references against codebase.
|
||||
@@ -0,0 +1,234 @@
|
||||
## Scope KMP iOS to 3-Domain Native Shell Parity
|
||||
|
||||
> Last updated: 2026-04-01
|
||||
> Status: Ready
|
||||
|
||||
- Workstream: kmp-revival
|
||||
- Backlog IDs: KR-02
|
||||
- Owner: TBD
|
||||
- Branch: TBD
|
||||
- PR: TBD
|
||||
|
||||
### Why
|
||||
|
||||
After KR-01, provider interfaces and `SdkProviderRegistry` live in `commonMain`, and `CryptoBridgeHandler` is shared. The iOS target benefits from these moves automatically, but still needs work: the WebView provider doesn't pass query params, handler registration is still wide (all 9 handlers), `isConfigured()` no longer requires all 8 providers (fixed in KR-01), and the iOS `SecureStorageBridgeHandler` has the same bare-value `get()` response bug as Android had.
|
||||
|
||||
You are scoping KMP iOS to 3-domain parity with native-shell-ios.
|
||||
|
||||
### Scope
|
||||
|
||||
- `packages/kmp-sdk/` (iOS target: `iosMain`)
|
||||
- `packages/self-sdk-swift/` (Swift provider implementations — query param support)
|
||||
- 3 bridge domains: `secureStorage`, `crypto`, `lifecycle`
|
||||
- Query param support for WebView URL loading
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Android target (done in KR-01)
|
||||
- `packages/native-shell-ios/` — do not modify
|
||||
- `packages/webview-bridge/` — bridge protocol unchanged
|
||||
- `packages/webview-app/` — WebView code unchanged
|
||||
- NFC, Camera, Biometric provider code in self-sdk-swift — retain but do not require registration
|
||||
|
||||
### Implementation Steps
|
||||
|
||||
#### 1. Fix SecureStorageBridgeHandler get() response shape (iOS)
|
||||
|
||||
**File:** `packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/handlers/SecureStorageBridgeHandler.kt`
|
||||
|
||||
**Current (line 44):** Returns `if (value != null) JsonPrimitive(value) else JsonNull` — bare primitive, same bug as Android had pre-KR-01.
|
||||
|
||||
**Required:** The TypeScript adapter at `webview-bridge/src/adapters/storage.ts:16` does `result?.value ?? null` — expects `{ value: string | null }`.
|
||||
|
||||
**Change:**
|
||||
```kotlin
|
||||
// Before (line 44)
|
||||
return if (value != null) JsonPrimitive(value) else JsonNull
|
||||
|
||||
// After
|
||||
return buildJsonObject {
|
||||
put("value", if (value != null) JsonPrimitive(value) else JsonNull)
|
||||
}
|
||||
```
|
||||
|
||||
Add import: `import kotlinx.serialization.json.buildJsonObject`
|
||||
|
||||
**Note:** After KR-01, the Android handler was fully rewritten to delegate via provider. The iOS handler already delegates via `SdkProviderRegistry.secureStorage` — only the response shape needs fixing.
|
||||
|
||||
**Alternative:** If both handlers are identical after this fix (same provider delegation, same response shape), consider moving `SecureStorageBridgeHandler` to `commonMain` to eliminate duplication. Check whether the iOS handler's `clear()` method is needed (Android handler has it, native-shell-android does not).
|
||||
|
||||
#### 2. Add query param support to WebViewProviderImpl
|
||||
|
||||
**File:** `packages/self-sdk-swift/Sources/SelfSdkSwift/WebViewProviderImpl.swift`
|
||||
|
||||
**Current:** `createWebView(onMessageReceived:isDebugMode:)` loads `Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "self-sdk-web")` without appending query params.
|
||||
|
||||
**Required:** native-shell-ios passes verification config as query params via `URLComponents`:
|
||||
```swift
|
||||
var components = URLComponents(url: fileURL, resolvingAgainstBaseURL: false)
|
||||
components?.query = queryParams
|
||||
```
|
||||
|
||||
**Change the method signature:**
|
||||
```swift
|
||||
// Before
|
||||
@objc(createWebViewOnMessageReceived:isDebugMode:)
|
||||
public func createWebView(onMessageReceived: @escaping (String) -> Void,
|
||||
isDebugMode: Bool) -> UIView
|
||||
|
||||
// After
|
||||
@objc(createWebViewOnMessageReceived:isDebugMode:queryParams:)
|
||||
public func createWebView(onMessageReceived: @escaping (String) -> Void,
|
||||
isDebugMode: Bool,
|
||||
queryParams: String? = nil) -> UIView
|
||||
```
|
||||
|
||||
**Update the URL loading logic** to append query params when provided:
|
||||
```swift
|
||||
if let htmlURL = Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "self-sdk-web") {
|
||||
var targetURL = htmlURL
|
||||
if let params = queryParams, !params.isEmpty {
|
||||
var components = URLComponents(url: htmlURL, resolvingAgainstBaseURL: false)
|
||||
components?.query = params
|
||||
targetURL = components?.url ?? htmlURL
|
||||
}
|
||||
wv.loadFileURL(targetURL, allowingReadAccessTo: htmlURL.deletingLastPathComponent())
|
||||
}
|
||||
```
|
||||
|
||||
Apply the same pattern to the debug URL (`http://localhost:5173`).
|
||||
|
||||
#### 3. Update WebViewProvider interface and IosWebViewHost
|
||||
|
||||
**File:** `packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/webview/IosWebViewHost.kt`
|
||||
|
||||
**Current (lines 18-29):** `createWebView()` calls `provider.createWebView(onMessageReceived, isDebugMode)` with no query params.
|
||||
|
||||
**Update `WebViewProvider` interface** (in `iosMain/kotlin/xyz/self/sdk/providers/WebViewProvider.kt` or wherever it's defined) to add the query param:
|
||||
```kotlin
|
||||
interface WebViewProvider {
|
||||
fun createWebView(
|
||||
onMessageReceived: (String) -> Unit,
|
||||
isDebugMode: Boolean,
|
||||
queryParams: String? = null,
|
||||
): UIView
|
||||
}
|
||||
```
|
||||
|
||||
**Update `IosWebViewHost`** to forward query params from the `VerificationRequest`:
|
||||
```kotlin
|
||||
fun createWebView(queryParams: String? = null): UIView {
|
||||
val provider = SdkProviderRegistry.webView
|
||||
?: throw IllegalStateException("WebView provider not configured")
|
||||
return provider.createWebView(
|
||||
onMessageReceived = { message -> router.onMessageReceived(message) },
|
||||
isDebugMode = isDebugMode,
|
||||
queryParams = queryParams,
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Scope handler registration to 3 domains
|
||||
|
||||
**File:** `packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/api/SelfSdk.ios.kt`
|
||||
|
||||
**Current (lines 145-158) registers 9 handlers:**
|
||||
```kotlin
|
||||
router.register(BiometricBridgeHandler())
|
||||
router.register(SecureStorageBridgeHandler())
|
||||
router.register(CryptoBridgeHandler())
|
||||
router.register(HapticBridgeHandler())
|
||||
router.register(AnalyticsBridgeHandler())
|
||||
router.register(lifecycleHandler)
|
||||
router.register(DocumentsBridgeHandler())
|
||||
router.register(CameraMrzBridgeHandler())
|
||||
router.register(NfcBridgeHandler(router))
|
||||
```
|
||||
|
||||
**Change to 3 handlers:**
|
||||
```kotlin
|
||||
router.register(SecureStorageBridgeHandler())
|
||||
router.register(CryptoBridgeHandler()) // now from commonMain after KR-01
|
||||
router.register(lifecycleHandler)
|
||||
```
|
||||
|
||||
Remove unused handler imports.
|
||||
|
||||
#### 5. Handle missing optional providers gracefully
|
||||
|
||||
After KR-01, `SdkProviderRegistry.isConfigured()` only requires `secureStorage` and `crypto`. But the remaining iOS handler files (Biometric, Haptic, etc.) still reference their providers. Since we're not registering those handlers, this is safe — but verify:
|
||||
|
||||
- The remaining handler files in `iosMain/handlers/` (BiometricBridgeHandler, HapticBridgeHandler, AnalyticsBridgeHandler, DocumentsBridgeHandler, CameraMrzBridgeHandler, NfcBridgeHandler) should still compile even though their providers may be nil. Since they're not registered, they won't be invoked.
|
||||
- If any of them are referenced at import time and cause initialization issues, add `@Suppress("unused")` or ensure they're only instantiated in the registration block.
|
||||
|
||||
#### 6. Build query params from VerificationRequest
|
||||
|
||||
**File:** `packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/api/SelfSdk.ios.kt`
|
||||
|
||||
The launch flow receives a `VerificationRequest`. Build query params from it and pass to `IosWebViewHost.createWebView(queryParams)`.
|
||||
|
||||
Reference the native-shell-ios `SelfSdkConfig.toQueryParams()` pattern for the field list. The KMP already has structured types, so use those rather than raw string extras.
|
||||
|
||||
### Files Modified
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `shared/src/iosMain/.../handlers/SecureStorageBridgeHandler.kt` | Fix `get()` response shape to `{ value: ... }` |
|
||||
| `packages/self-sdk-swift/.../WebViewProviderImpl.swift` | Add `queryParams` parameter, append to URL |
|
||||
| `shared/src/iosMain/.../webview/IosWebViewHost.kt` | Forward query params to provider |
|
||||
| `shared/src/iosMain/.../providers/WebViewProvider.kt` | Add `queryParams` to interface |
|
||||
| `shared/src/iosMain/.../api/SelfSdk.ios.kt` | Register only 3 handlers, build and pass query params |
|
||||
|
||||
### Files NOT Modified
|
||||
|
||||
- `packages/native-shell-ios/` — sibling, serves different consumers
|
||||
- `packages/webview-bridge/` — bridge protocol unchanged
|
||||
- `packages/webview-app/` — WebView code unchanged
|
||||
- Self-sdk-swift crypto/secureStorage providers — already match native-shell-ios functionality
|
||||
- `commonMain` files — already updated in KR-01
|
||||
|
||||
### Preconditions
|
||||
|
||||
- KR-01 complete (provider interfaces, SdkProviderRegistry, CryptoBridgeHandler in commonMain, MessageRouter version check)
|
||||
- `packages/webview-app/` builds and `dist/` output exists
|
||||
|
||||
### Validation
|
||||
|
||||
```bash
|
||||
# Build KMP iOS framework
|
||||
cd packages/kmp-sdk && ./gradlew :shared:linkDebugFrameworkIosSimulatorArm64
|
||||
|
||||
# Build XCFramework
|
||||
cd packages/kmp-sdk && ./gradlew createXCFramework
|
||||
|
||||
# Build self-sdk-swift
|
||||
cd packages/self-sdk-swift && swift build
|
||||
|
||||
# Run KMP common tests (includes shared handler tests)
|
||||
cd packages/kmp-sdk && ./gradlew :shared:jvmTest
|
||||
|
||||
# Verify SecureStorage response shape
|
||||
# Add unit test: iOS SecureStorageBridgeHandler.get() returns JsonObject with "value" key
|
||||
```
|
||||
|
||||
### Definition of Done
|
||||
|
||||
- [ ] iOS `SecureStorageBridgeHandler.get()` returns `{ value: string | null }` matching TypeScript adapter
|
||||
- [ ] `WebViewProviderImpl.createWebView()` accepts and appends query params
|
||||
- [ ] `IosWebViewHost` forwards query params from verification request
|
||||
- [ ] `WebViewProvider` interface includes `queryParams` parameter
|
||||
- [ ] Only 3 handlers registered on iOS (SecureStorage, Crypto, Lifecycle)
|
||||
- [ ] SDK does not crash if optional providers (NFC, Camera, etc.) are not registered
|
||||
- [ ] Query params built from VerificationRequest and passed to WebView
|
||||
- [ ] XCFramework builds cleanly
|
||||
- [ ] self-sdk-swift builds cleanly
|
||||
- [ ] All jvmTest tests pass
|
||||
|
||||
### Estimated PR Size
|
||||
|
||||
~200–300 LOC changed. Within the 1k–3k target.
|
||||
|
||||
### Status Log
|
||||
|
||||
- 2026-03-31: Plan created.
|
||||
- 2026-04-01: Updated to reflect KR-01 provider delegation changes. CryptoBridgeHandler now comes from commonMain. SdkProviderRegistry isConfigured() already fixed. SecureStorage response shape bug confirmed on iOS. Added query param building from VerificationRequest.
|
||||
@@ -0,0 +1,169 @@
|
||||
## Validate Build Artifacts and Test App
|
||||
|
||||
> Last updated: 2026-04-01
|
||||
> Status: Ready
|
||||
|
||||
- Workstream: kmp-revival
|
||||
- Backlog IDs: KR-03
|
||||
- Owner: TBD
|
||||
- Branch: TBD
|
||||
- PR: TBD
|
||||
|
||||
### Why
|
||||
|
||||
After KR-01 (Android parity) and KR-02 (iOS parity), you need to validate that the scoped KMP SDK produces clean publishable artifacts and that the test app exercises the full 3-domain WebView flow on both platforms. This validates that a KMP consumer can actually integrate the SDK end-to-end.
|
||||
|
||||
### Scope
|
||||
|
||||
- `packages/kmp-sdk/` — build artifact validation
|
||||
- `packages/kmp-sdk-test-app/` — test app adaptation for 3-domain scope
|
||||
- `packages/self-sdk-swift/` — ensure it integrates cleanly with scoped KMP
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Actual publishing to external Maven/SPM registries (that's a follow-up, equivalent to paused NS-08)
|
||||
- `packages/native-shell-android/` and `packages/native-shell-ios/` — do not modify
|
||||
- CI/CD pipeline changes
|
||||
|
||||
### Implementation Steps
|
||||
|
||||
#### 1. Validate Android AAR build
|
||||
|
||||
```bash
|
||||
cd packages/kmp-sdk
|
||||
|
||||
# Debug build
|
||||
./gradlew :shared:assembleDebug
|
||||
|
||||
# Release build
|
||||
./gradlew :shared:assembleRelease
|
||||
|
||||
# Verify artifact exists and is reasonable size
|
||||
ls -la shared/build/outputs/aar/shared-release.aar
|
||||
# Should be significantly smaller than before (NFC/camera/biometric deps removed)
|
||||
|
||||
# Local Maven publish
|
||||
./gradlew :shared:publishToMavenLocal
|
||||
ls ~/.m2/repository/xyz/self/sdk/shared/
|
||||
```
|
||||
|
||||
#### 2. Validate iOS XCFramework build
|
||||
|
||||
```bash
|
||||
cd packages/kmp-sdk
|
||||
|
||||
# Framework build
|
||||
./gradlew :shared:linkDebugFrameworkIosSimulatorArm64
|
||||
|
||||
# XCFramework (requires both arm64 and simulator targets)
|
||||
./gradlew createXCFramework
|
||||
|
||||
# Verify output
|
||||
ls -la shared/build/XCFrameworks/
|
||||
```
|
||||
|
||||
#### 3. Adapt kmp-sdk-test-app for 3-domain scope
|
||||
|
||||
**Location:** `packages/kmp-sdk-test-app/`
|
||||
|
||||
The test app currently exercises all KMP handlers including NFC and camera. Update it to only use the 3-domain scope.
|
||||
|
||||
**Android (`packages/kmp-sdk-test-app/androidApp/`):**
|
||||
- Remove NFC and Camera permission requests from AndroidManifest.xml (if present)
|
||||
- The app should rely on default providers (Activity auto-initializes `EncryptedSharedPreferencesProvider` and `AndroidKeystoreCryptoProvider` if not set). Alternatively, demonstrate explicit provider registration before launch:
|
||||
```kotlin
|
||||
SdkProviderRegistry.secureStorage = EncryptedSharedPreferencesProvider(context)
|
||||
SdkProviderRegistry.crypto = AndroidKeystoreCryptoProvider()
|
||||
```
|
||||
- Verify the app launches the SDK, loads the WebView, and handles lifecycle callbacks
|
||||
- Verify crypto operations work (generateKey, getPublicKey, sign)
|
||||
- Verify secure storage operations work (get, set, remove)
|
||||
|
||||
**iOS (`packages/kmp-sdk-test-app/iosApp/`):**
|
||||
- Register only required providers: `secureStorage`, `crypto`, `webView`
|
||||
- Remove registration of NFC, Camera, Biometric, Haptic, Documents providers
|
||||
- Verify the app launches the SDK, loads the WebView, and handles lifecycle callbacks
|
||||
- Ensure `self-sdk-web/` assets are in Copy Bundle Resources
|
||||
|
||||
#### 4. Run full test suite
|
||||
|
||||
```bash
|
||||
# Common tests (bridge, routing, serialization, lifecycle)
|
||||
cd packages/kmp-sdk && ./gradlew :shared:jvmTest
|
||||
|
||||
# Verify no test failures from scoping changes
|
||||
# Tests for NFC, Camera, MRZ should still pass (they test code, not registration)
|
||||
```
|
||||
|
||||
#### 5. Update OVERVIEW.md module table
|
||||
|
||||
**File:** `specs/projects/sdk/OVERVIEW.md`
|
||||
|
||||
Update the module table (around line 110) to reflect KMP revival:
|
||||
|
||||
**Already done** — OVERVIEW.md module table was updated as part of the spec review on 2026-04-01. Verify it still reflects:
|
||||
|
||||
| Module | Status | Notes |
|
||||
|--------|--------|-------|
|
||||
| KMP Native Shell (`packages/kmp-sdk/`) | Active (3-domain scope) | Serves KMP consumers, provider delegation on both platforms |
|
||||
| Swift Providers (`packages/self-sdk-swift/`) | Active | iOS providers for KMP |
|
||||
| KMP Test App (`packages/kmp-sdk-test-app/`) | Active | E2E harness |
|
||||
|
||||
### Files Modified
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `packages/kmp-sdk-test-app/` (multiple files) | Scope to 3-domain handlers/providers |
|
||||
| `specs/projects/sdk/OVERVIEW.md` | Update module table with KMP status |
|
||||
|
||||
### Files NOT Modified
|
||||
|
||||
- `packages/kmp-sdk/` — no code changes (KR-01 and KR-02 handle this)
|
||||
- `packages/native-shell-*` — sibling implementations, unchanged
|
||||
|
||||
### Preconditions
|
||||
|
||||
- KR-01 complete (Android parity)
|
||||
- KR-02 complete (iOS parity)
|
||||
|
||||
### Validation
|
||||
|
||||
```bash
|
||||
# Full Android validation
|
||||
cd packages/kmp-sdk && ./gradlew :shared:assembleRelease && ./gradlew :shared:jvmTest && ./gradlew :shared:publishToMavenLocal
|
||||
|
||||
# Full iOS validation
|
||||
cd packages/kmp-sdk && ./gradlew createXCFramework
|
||||
cd packages/self-sdk-swift && swift build
|
||||
|
||||
# Test app Android build
|
||||
cd packages/kmp-sdk-test-app/androidApp && ../gradlew :androidApp:assembleDebug
|
||||
|
||||
# Test app iOS build (Xcode required)
|
||||
# Open packages/kmp-sdk-test-app/iosApp/ in Xcode and build for simulator
|
||||
```
|
||||
|
||||
### Definition of Done
|
||||
|
||||
- [ ] Android AAR builds (debug + release) with no errors
|
||||
- [ ] iOS XCFramework builds with no errors
|
||||
- [ ] Local Maven publish succeeds
|
||||
- [ ] All jvmTest tests pass
|
||||
- [ ] Test app Android builds and runs
|
||||
- [ ] Test app iOS builds and runs
|
||||
- [ ] Crypto operations work in test app (generateKey, sign, getPublicKey)
|
||||
- [ ] SecureStorage operations work in test app (get, set, remove)
|
||||
- [ ] Lifecycle operations work in test app (ready, dismiss, setResult)
|
||||
- [ ] OVERVIEW.md module table updated
|
||||
- [ ] AAR size is smaller than pre-scoping (NFC/camera deps removed)
|
||||
- [ ] Provider delegation works end-to-end on Android (default providers)
|
||||
- [ ] Provider delegation works end-to-end on iOS (self-sdk-swift providers)
|
||||
|
||||
### Estimated PR Size
|
||||
|
||||
~200 LOC changed. Within the 1k–3k target.
|
||||
|
||||
### Status Log
|
||||
|
||||
- 2026-03-31: Plan created.
|
||||
- 2026-04-01: Updated for provider delegation (KR-01 change). Added provider E2E validation. OVERVIEW.md already updated.
|
||||
Reference in New Issue
Block a user