udpate specs (#1909)

This commit is contained in:
Justin Hernandez
2026-04-01 17:53:51 -07:00
committed by GitHub
parent c5e3b03e42
commit 737f39d955
5 changed files with 205 additions and 118 deletions

View File

@@ -115,20 +115,20 @@ See [KMP Revival Spec](./workstreams/kmp-revival/SPEC.md) for details.
## Module Table
| Module | Location | Status | Current Role | Action Needed |
| -------------------- | ----------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------- | ------------------------------------------- |
| WebView UI | `packages/webview-app/` | Active | Primary product surface, route orchestration, mock-first screen migration | Finish remaining UI migration specs/routes |
| SDK Core | `packages/mobile-sdk-alpha/` | Active | Shared engine for WebView/browser delivery | Keep browser entry clean and request-driven |
| WebView Bridge | `packages/webview-bridge/` | Active | Host callback surface for future lifecycle wiring | Stable for current UI pass |
| 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/` | 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 |
| 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 |
| Module | Location | Status | Current Role | Action Needed |
| -------------------- | ----------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------- | ---------------------------------------------- |
| WebView UI | `packages/webview-app/` | Active | Primary product surface, route orchestration, mock-first screen migration | Finish remaining UI migration specs/routes |
| SDK Core | `packages/mobile-sdk-alpha/` | Active | Shared engine for WebView/browser delivery | Keep browser entry clean and request-driven |
| WebView Bridge | `packages/webview-bridge/` | Active | Host callback surface for future lifecycle wiring | Stable for current UI pass |
| 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/` | 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 |
| 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

View File

@@ -15,15 +15,15 @@
## 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 |
| 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.
@@ -54,19 +54,19 @@ Both options are valid. KMP has more infrastructure already built; native-shells
## 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 |
| 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 |
| 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`
@@ -84,16 +84,16 @@ Allowed statuses: `Ready`, `In Progress`, `Blocked`, `Deferred`, `Done`
## 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 |
| 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
@@ -101,35 +101,35 @@ 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` |
| 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) |
| 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) |
| 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 |
| 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 |

View File

@@ -14,6 +14,7 @@
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
@@ -26,6 +27,7 @@ You are closing these gaps by:
- Provider delegation: move interfaces to commonMain, add default Android implementations
- WebView host upgrades (WebChromeClient, query params)
- Bridge alignment (response shapes, protocol version)
- Android SDK slimming needed to make the 3-domain scope real: stop registering unused handlers, remove out-of-scope Android permissions, and drop no-longer-needed Android dependencies from the published artifact
### Out of Scope
@@ -43,12 +45,14 @@ You are closing these gaps by:
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?
@@ -59,6 +63,7 @@ interface SecureStorageProvider {
```
**Current `CryptoProvider` interface (iosMain):**
```kotlin
interface CryptoProvider {
fun generateKey(keyRef: String)
@@ -79,6 +84,7 @@ No changes to the interfaces themselves — just the source set location.
**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
@@ -116,6 +122,7 @@ object SdkProviderRegistry {
```
**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
@@ -301,6 +308,7 @@ class SecureStorageBridgeHandler : BridgeHandler {
```
**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`
@@ -396,6 +404,7 @@ class CryptoBridgeHandler : BridgeHandler {
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)
@@ -405,6 +414,7 @@ Response shapes match `webview-bridge/src/adapters/crypto.ts`:
**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))
@@ -416,6 +426,7 @@ private fun registerHandlers() {
```
**Change to:**
```kotlin
private fun registerHandlers() {
router.register(SecureStorageBridgeHandler()) // no Context — delegates to provider
@@ -425,6 +436,7 @@ private fun registerHandlers() {
```
**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:
@@ -448,6 +460,25 @@ This gives consumers the option to set their own providers before launching the
- Remove unused imports: `BiometricBridgeHandler`, `CameraMrzBridgeHandler`, `NfcBridgeHandler`, `Manifest`, `PackageManager`, `ActivityResultContracts`, `ContextCompat`.
- Add imports: `CryptoBridgeHandler`, `SdkProviderRegistry`, `EncryptedSharedPreferencesProvider`, `AndroidKeystoreCryptoProvider`.
#### 6a. Trim Android manifest and dependency surface
This ownership belongs to KR-01, not KR-03. KR-03 validates the trimmed artifact; KR-01 performs the actual slimming.
**Android manifest**
- Remove out-of-scope permissions and features from `shared/src/androidMain/AndroidManifest.xml` that are only needed for NFC, camera/MRZ, or haptics.
- Keep only permissions and features required by the scoped 3-domain Android delivery.
**Gradle dependencies**
- Remove Android dependencies from `shared/build.gradle.kts` that only exist to support out-of-scope handlers once those handlers are no longer registered.
- Expected removals include NFC/passport-reading, camera/MRZ, and biometric-specific dependencies if no remaining 3-domain code path needs them.
- Keep Android WebView, activity/lifecycle, and encrypted storage dependencies required by the scoped SDK.
**Artifact expectation**
- `shared-release.aar` should become smaller after this step because out-of-scope Android code paths and dependencies are no longer pulled into the published artifact.
#### 7. Add query param support to WebView URL loading
**File:** `packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/AndroidWebViewHost.kt`
@@ -457,6 +488,7 @@ This gives consumers the option to set their own providers before launching the
**Change to:** `fun createWebView(queryParams: String = ""): WebView`
**Update loadUrl calls (lines 129-139):**
```kotlin
// Before
webView.loadUrl("https://appassets.androidplatform.net/index.html")
@@ -472,6 +504,7 @@ 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
@@ -551,12 +584,14 @@ webChromeClient = object : WebChromeClient() {
```
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
@@ -565,6 +600,7 @@ companion object {
```
**Also add permission result handling** to `SelfVerificationActivity`:
```kotlin
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
@@ -599,6 +635,7 @@ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
**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)
@@ -616,6 +653,7 @@ if (request.version != BRIDGE_PROTOCOL_VERSION) {
```
Add companion object constant (matching `webview-bridge/src/types.ts:152`):
```kotlin
companion object {
const val BRIDGE_PROTOCOL_VERSION = 1
@@ -629,6 +667,7 @@ companion object {
**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")
@@ -647,6 +686,7 @@ companion object {
```
**Keep:**
- `androidx.webkit:webkit` — WebView
- `androidx.security:security-crypto` — Default EncryptedSharedPreferencesProvider
- `androidx.appcompat:appcompat` — Activity
@@ -657,33 +697,33 @@ companion object {
### 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 |
| 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 |
| 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 |
| 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

View File

@@ -29,6 +29,7 @@ You are scoping KMP iOS to 3-domain parity with native-shell-ios.
- `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
- Removing retained Swift-side NFC / biometrics code or dependencies from `packages/self-sdk-swift/` — the short-term strategy is "retain but do not register" to avoid broad Swift package churn during parity work
### Implementation Steps
@@ -41,6 +42,7 @@ You are scoping KMP iOS to 3-domain parity with native-shell-ios.
**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
@@ -64,12 +66,14 @@ Add import: `import kotlinx.serialization.json.buildJsonObject`
**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:)
@@ -84,6 +88,7 @@ public func createWebView(onMessageReceived: @escaping (String) -> Void,
```
**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
@@ -105,6 +110,7 @@ Apply the same pattern to the debug URL (`http://localhost:5173`).
**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(
@@ -116,6 +122,7 @@ interface WebViewProvider {
```
**Update `IosWebViewHost`** to forward query params from the `VerificationRequest`:
```kotlin
fun createWebView(queryParams: String? = null): UIView {
val provider = SdkProviderRegistry.webView
@@ -133,6 +140,7 @@ fun createWebView(queryParams: String? = null): UIView {
**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())
@@ -146,6 +154,7 @@ router.register(NfcBridgeHandler(router))
```
**Change to 3 handlers:**
```kotlin
router.register(SecureStorageBridgeHandler())
router.register(CryptoBridgeHandler()) // now from commonMain after KR-01
@@ -169,15 +178,23 @@ The launch flow receives a `VerificationRequest`. Build query params from it and
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.
#### 7. Preserve retained Swift providers without requiring them
`packages/self-sdk-swift/` still contains provider implementations and dependencies for NFC, camera/MRZ, biometrics, haptics, and documents. KR-02 does not remove that code. The parity requirement is narrower:
- Only `secureStorage`, `crypto`, and `webView` must be registered for the scoped KMP flow.
- Retained Swift providers may continue to compile and ship, but they must not be required by KMP startup or `SdkProviderRegistry.isConfigured()`.
- Document this explicitly in implementation notes so the temporary iOS package footprint is not mistaken for a spec bug.
### 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 |
| 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
@@ -186,6 +203,7 @@ Reference the native-shell-ios `SelfSdkConfig.toQueryParams()` pattern for the f
- `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
- Removal of retained NFC / camera / biometric Swift provider code and dependencies — deferred follow-up, not part of KR-02
### Preconditions
@@ -219,6 +237,7 @@ cd packages/kmp-sdk && ./gradlew :shared:jvmTest
- [ ] `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
- [ ] `self-sdk-swift` may retain optional provider code/dependencies, but KMP startup only requires `secureStorage`, `crypto`, and `webView`
- [ ] Query params built from VerificationRequest and passed to WebView
- [ ] XCFramework builds cleanly
- [ ] self-sdk-swift builds cleanly

View File

@@ -16,14 +16,16 @@ After KR-01 (Android parity) and KR-02 (iOS parity), you need to validate that t
### 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
- `packages/kmp-sdk-test-app/`simplify the test harness to validate only the 3-domain contract end-to-end
- `packages/self-sdk-swift/` — ensure it integrates cleanly with scoped KMP without requiring registration of optional providers
### 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
- Android dependency trimming or Android manifest slimming inside `packages/kmp-sdk/` — that ownership belongs to KR-01; KR-03 validates the result
- Removing retained NFC / biometric Swift provider code or dependencies from `packages/self-sdk-swift/` — validate current integration shape, do not redesign it here
### Implementation Steps
@@ -59,33 +61,55 @@ cd packages/kmp-sdk
./gradlew createXCFramework
# Verify output
ls -la shared/build/XCFrameworks/
ls -la shared/build/xcframework/
```
#### 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.
The current test app still contains a full MRZ/NFC-oriented verification flow. KR-03 should simplify the harness so its primary job is validating the 3-domain KMP contract, not preserving the older full-verification demo.
**Android (`packages/kmp-sdk-test-app/androidApp/`):**
- Remove NFC and Camera permission requests from AndroidManifest.xml (if present)
**Decision:** gut the MRZ/NFC-first flow from the default harness rather than gating it behind a flag. Keep the test app focused on `secureStorage`, `crypto`, and `lifecycle`.
**Expected harness behavior**
- The default app flow should expose a simple, repeatable smoke-test path for the 3 bridge domains.
- Avoid UI paths that require camera, NFC, biometrics, or documents to demonstrate success.
- Existing MRZ/NFC-specific state, screens, and tests may be removed or aggressively simplified if they do not serve the 3-domain validation goal.
**Android (`packages/kmp-sdk-test-app/composeApp/` and platform wrappers):**
- Remove test-app-level NFC and camera permission requests from Android manifests if they are only used by the deleted MRZ/NFC flows
- 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)
- Provide a visible pass/fail mechanism for each scoped domain check
**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
- Do not require registration of retained optional providers from `self-sdk-swift`
#### 4. Run full test suite
#### 4. Add scripted domain smoke checks to the test app
Manual "launch and eyeball it" validation is not enough. Add one explicit smoke check per scoped domain, surfaced through the test app UI or logs so a human can run the same sequence on both platforms.
**Required checks**
- `secureStorage`: write a value, read it back, remove it, then confirm a subsequent read returns `null`
- `crypto`: generate a key, fetch the public key, sign a message, and log the returned artifacts
- `lifecycle`: trigger `ready`, then `setResult`, and confirm the Activity / view controller returns control to the host with the expected result payload
These do not need to be Espresso/XCUITest. A deterministic in-app smoke-test button or scripted harness flow is sufficient if it produces clear pass/fail output.
#### 5. Run full test suite
```bash
# Common tests (bridge, routing, serialization, lifecycle)
@@ -95,7 +119,7 @@ cd packages/kmp-sdk && ./gradlew :shared:jvmTest
# Tests for NFC, Camera, MRZ should still pass (they test code, not registration)
```
#### 5. Update OVERVIEW.md module table
#### 6. Update OVERVIEW.md module table
**File:** `specs/projects/sdk/OVERVIEW.md`
@@ -103,23 +127,24 @@ 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 |
| 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 |
| File | Change |
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `packages/kmp-sdk-test-app/` (multiple files) | Remove MRZ/NFC-first default flow, scope harness to 3-domain handlers/providers, add scripted domain checks |
| `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/kmp-sdk/` — no new feature work; KR-03 only validates the artifact shape produced by KR-01 and KR-02
- `packages/native-shell-*` — sibling implementations, unchanged
- `packages/self-sdk-swift/` retained optional provider code/dependencies — validate integration shape, do not slim package contents here
### Preconditions
@@ -137,7 +162,7 @@ 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
cd packages/kmp-sdk-test-app && ./gradlew :composeApp:assembleDebug
# Test app iOS build (Xcode required)
# Open packages/kmp-sdk-test-app/iosApp/ in Xcode and build for simulator
@@ -151,13 +176,15 @@ cd packages/kmp-sdk-test-app/androidApp && ../gradlew :androidApp:assembleDebug
- [ ] 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)
- [ ] Default test-app flow no longer depends on MRZ/NFC/camera features
- [ ] Test app exposes deterministic smoke checks for each scoped domain
- [ ] Crypto smoke check passes in test app (generateKey, sign, getPublicKey)
- [ ] SecureStorage smoke check passes in test app (get, set, remove)
- [ ] Lifecycle smoke check passes in test app (`ready`, `setResult`, host returns to caller)
- [ ] OVERVIEW.md module table updated
- [ ] AAR size is smaller than pre-scoping (NFC/camera deps removed)
- [ ] AAR size is smaller than pre-scoping (validated result of KR-01 Android slimming)
- [ ] Provider delegation works end-to-end on Android (default providers)
- [ ] Provider delegation works end-to-end on iOS (self-sdk-swift providers)
- [ ] Provider delegation works end-to-end on iOS (`self-sdk-swift` registers only the required providers for scoped flow)
### Estimated PR Size
@@ -167,3 +194,4 @@ cd packages/kmp-sdk-test-app/androidApp && ../gradlew :androidApp:assembleDebug
- 2026-03-31: Plan created.
- 2026-04-01: Updated for provider delegation (KR-01 change). Added provider E2E validation. OVERVIEW.md already updated.
- 2026-04-01: Clarified XCFramework output path, moved Android slimming ownership fully into KR-01, and made KR-03 require a simplified 3-domain test harness with explicit smoke checks.