From 737f39d955a623eb3aff190a3ca904b8befd3d8f Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Wed, 1 Apr 2026 17:53:51 -0700 Subject: [PATCH] udpate specs (#1909) --- specs/projects/sdk/OVERVIEW.md | 28 ++--- .../sdk/workstreams/kmp-revival/SPEC.md | 100 +++++++++--------- .../kmp-revival/plans/KR-01-android-parity.md | 82 ++++++++++---- .../kmp-revival/plans/KR-02-ios-parity.md | 33 ++++-- .../plans/KR-03-validate-and-publish.md | 80 +++++++++----- 5 files changed, 205 insertions(+), 118 deletions(-) diff --git a/specs/projects/sdk/OVERVIEW.md b/specs/projects/sdk/OVERVIEW.md index 7f449973a..ac34e0e5b 100644 --- a/specs/projects/sdk/OVERVIEW.md +++ b/specs/projects/sdk/OVERVIEW.md @@ -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 diff --git a/specs/projects/sdk/workstreams/kmp-revival/SPEC.md b/specs/projects/sdk/workstreams/kmp-revival/SPEC.md index 95ce74984..b68e9b00b 100644 --- a/specs/projects/sdk/workstreams/kmp-revival/SPEC.md +++ b/specs/projects/sdk/workstreams/kmp-revival/SPEC.md @@ -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 | diff --git a/specs/projects/sdk/workstreams/kmp-revival/plans/KR-01-android-parity.md b/specs/projects/sdk/workstreams/kmp-revival/plans/KR-01-android-parity.md index 1c59cc4ae..1452ea618 100644 --- a/specs/projects/sdk/workstreams/kmp-revival/plans/KR-01-android-parity.md +++ b/specs/projects/sdk/workstreams/kmp-revival/plans/KR-01-android-parity.md @@ -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>? = 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, 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(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 diff --git a/specs/projects/sdk/workstreams/kmp-revival/plans/KR-02-ios-parity.md b/specs/projects/sdk/workstreams/kmp-revival/plans/KR-02-ios-parity.md index 3d74ea927..b75eeee02 100644 --- a/specs/projects/sdk/workstreams/kmp-revival/plans/KR-02-ios-parity.md +++ b/specs/projects/sdk/workstreams/kmp-revival/plans/KR-02-ios-parity.md @@ -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 diff --git a/specs/projects/sdk/workstreams/kmp-revival/plans/KR-03-validate-and-publish.md b/specs/projects/sdk/workstreams/kmp-revival/plans/KR-03-validate-and-publish.md index 60b4aa625..4529411e2 100644 --- a/specs/projects/sdk/workstreams/kmp-revival/plans/KR-03-validate-and-publish.md +++ b/specs/projects/sdk/workstreams/kmp-revival/plans/KR-03-validate-and-publish.md @@ -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.