mirror of
https://github.com/selfxyz/self.git
synced 2026-04-27 03:01:15 -04:00
Wire WebView camera adapter and harden Android SDK launch flow (#1805)
* save chunk 1e work * chunk 2f * pr feedback * fix pr feedback * cr feedback * remove dupe var * feedback from cr * add kmp:start dx helper * save web consolidtion work for later * add specs * update * pr fixes * update reorg plan
This commit is contained in:
@@ -11,4 +11,3 @@ MIXPANEL_NFC_PROJECT_TOKEN=
|
||||
SEGMENT_KEY=
|
||||
SENTRY_DSN=
|
||||
SUMSUB_TEE_URL=
|
||||
IS_TEST_BUILD=
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"kmp:format": "yarn workspace @selfxyz/kmp-test-app format",
|
||||
"kmp:ios": "yarn workspace @selfxyz/kmp-test-app ios:open",
|
||||
"kmp:lint": "yarn workspace @selfxyz/kmp-test-app lint",
|
||||
"kmp:start": "bash scripts/kmp-start.sh",
|
||||
"kmp:test": "yarn workspace @selfxyz/kmp-sdk test",
|
||||
"lint": "yarn lint:headers && yarn workspaces foreach --parallel -i --all --exclude self-workspace-root run lint",
|
||||
"lint:headers": "node scripts/check-duplicate-headers.cjs . && node scripts/check-license-headers.mjs . --check",
|
||||
|
||||
@@ -4,13 +4,16 @@
|
||||
|
||||
package xyz.self.sdk.api
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import kotlinx.serialization.json.Json
|
||||
import xyz.self.sdk.webview.SelfVerificationActivity
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* Android implementation of the Self SDK.
|
||||
@@ -20,44 +23,65 @@ actual class SelfSdk private constructor(
|
||||
private val config: SelfSdkConfig,
|
||||
) {
|
||||
private var activityLauncher: ActivityResultLauncher<Intent>? = null
|
||||
private var launcherOwner: WeakReference<ComponentActivity>? = null
|
||||
private var boundActivity: WeakReference<ComponentActivity>? = null
|
||||
private var pendingCallback: SelfSdkCallback? = null
|
||||
private var lifecycleObserver: DefaultLifecycleObserver? = null
|
||||
private var observerActivity: WeakReference<ComponentActivity>? = null
|
||||
|
||||
actual companion object {
|
||||
private var instance: SelfSdk? = null
|
||||
private var configuredWith: SelfSdkConfig? = null
|
||||
private var currentActivity: WeakReference<ComponentActivity>? = null
|
||||
|
||||
/**
|
||||
* Configures and returns a singleton SelfSdk instance.
|
||||
*/
|
||||
actual fun configure(config: SelfSdkConfig): SelfSdk {
|
||||
if (instance == null) {
|
||||
if (instance == null || configuredWith != config) {
|
||||
instance?.cleanup()
|
||||
instance = SelfSdk(config)
|
||||
configuredWith = config
|
||||
}
|
||||
val activity = currentActivity?.get()
|
||||
if (activity != null) {
|
||||
instance?.bindActivity(activity)
|
||||
}
|
||||
return instance!!
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the currently active host Activity so common launch(request, callback)
|
||||
* can work without Android-specific overloads.
|
||||
*/
|
||||
fun bindActivity(activity: ComponentActivity) {
|
||||
currentActivity = WeakReference(activity)
|
||||
instance?.bindActivity(activity)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the verification flow.
|
||||
* The calling Activity must be a ComponentActivity for result handling.
|
||||
*
|
||||
* Note: For production use, the host app should register the ActivityResultLauncher
|
||||
* in onCreate() and pass it to this method, rather than registering it here.
|
||||
* This implementation is simplified for the initial version.
|
||||
* Launches the verification flow through the common API surface.
|
||||
* On Android, this requires a bound ComponentActivity via SelfSdk.bindActivity(activity).
|
||||
*/
|
||||
actual fun launch(
|
||||
request: VerificationRequest,
|
||||
callback: SelfSdkCallback,
|
||||
) {
|
||||
// Store callback for later
|
||||
pendingCallback = callback
|
||||
|
||||
// Get current activity context
|
||||
// Note: In production, the host app should pass the activity explicitly
|
||||
// For now, we'll require the activity to be passed via a helper method
|
||||
throw NotImplementedError(
|
||||
"Please use launch(activity, request, callback) instead. " +
|
||||
"The Activity parameter is required on Android.",
|
||||
)
|
||||
val activity =
|
||||
resolveActivity()
|
||||
?: run {
|
||||
callback.onFailure(
|
||||
SelfSdkError(
|
||||
code = "MISSING_ACTIVITY",
|
||||
message =
|
||||
"No bound ComponentActivity found. " +
|
||||
"Call SelfSdk.bindActivity(activity) in your Activity before launch().",
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
launchInternal(activity, request, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,6 +97,28 @@ actual class SelfSdk private constructor(
|
||||
request: VerificationRequest,
|
||||
callback: SelfSdkCallback,
|
||||
) {
|
||||
bindActivity(activity)
|
||||
Companion.currentActivity = WeakReference(activity)
|
||||
launchInternal(activity, request, callback)
|
||||
}
|
||||
|
||||
private fun launchInternal(
|
||||
activity: ComponentActivity,
|
||||
request: VerificationRequest,
|
||||
callback: SelfSdkCallback,
|
||||
) {
|
||||
if (pendingCallback != null) {
|
||||
callback.onFailure(
|
||||
SelfSdkError(
|
||||
code = "VERIFICATION_IN_PROGRESS",
|
||||
message = "A verification flow is already in progress",
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
pendingCallback = callback
|
||||
|
||||
// Create intent for SelfVerificationActivity
|
||||
val intent =
|
||||
Intent(activity, SelfVerificationActivity::class.java).apply {
|
||||
@@ -81,21 +127,120 @@ actual class SelfSdk private constructor(
|
||||
putExtra(SelfVerificationActivity.EXTRA_CONFIG, serializeConfig(config))
|
||||
}
|
||||
|
||||
// Register using the ActivityResultRegistry directly (without LifecycleOwner)
|
||||
// so it can be called after onStart(). The host app's Activity may already
|
||||
// be in RESUMED state when the user taps "verify".
|
||||
if (activityLauncher == null) {
|
||||
activityLauncher =
|
||||
activity.activityResultRegistry.register(
|
||||
"self-sdk-verification",
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
) { result ->
|
||||
handleActivityResult(result.resultCode, result.data, callback)
|
||||
}
|
||||
// Launch the verification activity
|
||||
val launcher = activityLauncher
|
||||
if (launcher == null) {
|
||||
pendingCallback = null
|
||||
callback.onFailure(
|
||||
SelfSdkError(
|
||||
code = "LAUNCHER_NOT_AVAILABLE",
|
||||
message = "Could not initialize Android activity launcher",
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
try {
|
||||
launcher.launch(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
pendingCallback = null
|
||||
callback.onFailure(
|
||||
SelfSdkError(
|
||||
code = "ACTIVITY_NOT_FOUND",
|
||||
message = "Could not launch verification activity: ${e.message}",
|
||||
),
|
||||
)
|
||||
} catch (e: IllegalStateException) {
|
||||
pendingCallback = null
|
||||
callback.onFailure(
|
||||
SelfSdkError(
|
||||
code = "LAUNCH_FAILED",
|
||||
message = "Could not launch verification activity: ${e.message}",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindActivity(activity: ComponentActivity) {
|
||||
boundActivity = WeakReference(activity)
|
||||
ensureLauncher(activity)
|
||||
|
||||
if (observerActivity?.get() === activity) {
|
||||
return
|
||||
}
|
||||
|
||||
// Launch the verification activity
|
||||
activityLauncher?.launch(intent)
|
||||
val previousActivity = observerActivity?.get()
|
||||
val previousObserver = lifecycleObserver
|
||||
if (previousActivity != null && previousObserver != null) {
|
||||
previousActivity.lifecycle.removeObserver(previousObserver)
|
||||
}
|
||||
|
||||
val observer =
|
||||
object : DefaultLifecycleObserver {
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
if (launcherOwner?.get() === activity) {
|
||||
activityLauncher?.unregister()
|
||||
activityLauncher = null
|
||||
launcherOwner = null
|
||||
}
|
||||
if (boundActivity?.get() === activity) {
|
||||
boundActivity = null
|
||||
}
|
||||
pendingCallback?.onCancelled()
|
||||
pendingCallback = null
|
||||
lifecycleObserver = null
|
||||
observerActivity = null
|
||||
}
|
||||
}
|
||||
lifecycleObserver = observer
|
||||
observerActivity = WeakReference(activity)
|
||||
activity.lifecycle.addObserver(observer)
|
||||
}
|
||||
|
||||
private fun cleanup() {
|
||||
activityLauncher?.unregister()
|
||||
activityLauncher = null
|
||||
launcherOwner = null
|
||||
|
||||
val activity = observerActivity?.get()
|
||||
val observer = lifecycleObserver
|
||||
if (activity != null && observer != null) {
|
||||
activity.lifecycle.removeObserver(observer)
|
||||
}
|
||||
lifecycleObserver = null
|
||||
observerActivity = null
|
||||
boundActivity = null
|
||||
|
||||
pendingCallback?.onCancelled()
|
||||
pendingCallback = null
|
||||
}
|
||||
|
||||
private fun resolveActivity(): ComponentActivity? {
|
||||
val resolved = boundActivity?.get() ?: Companion.currentActivity?.get()
|
||||
if (resolved != null) {
|
||||
bindActivity(resolved)
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
private fun ensureLauncher(activity: ComponentActivity) {
|
||||
val currentOwner = launcherOwner?.get()
|
||||
if (activityLauncher != null && currentOwner === activity) {
|
||||
return
|
||||
}
|
||||
|
||||
activityLauncher?.unregister()
|
||||
launcherOwner = WeakReference(activity)
|
||||
activityLauncher =
|
||||
activity.activityResultRegistry.register(
|
||||
"self-sdk-verification",
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
) { result ->
|
||||
val callback = pendingCallback
|
||||
pendingCallback = null
|
||||
if (callback != null) {
|
||||
handleActivityResult(result.resultCode, result.data, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,7 +252,7 @@ actual class SelfSdk private constructor(
|
||||
callback: SelfSdkCallback,
|
||||
) {
|
||||
when (resultCode) {
|
||||
Activity.RESULT_OK -> {
|
||||
SelfVerificationActivity.RESULT_CODE_SUCCESS -> {
|
||||
val resultDataJson = data?.getStringExtra(SelfVerificationActivity.EXTRA_RESULT_DATA)
|
||||
val resultType = data?.getStringExtra(SelfVerificationActivity.EXTRA_RESULT_TYPE)
|
||||
if (resultDataJson != null) {
|
||||
@@ -135,7 +280,7 @@ actual class SelfSdk private constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
Activity.RESULT_CANCELED -> {
|
||||
SelfVerificationActivity.RESULT_CODE_CANCELLED -> {
|
||||
// User cancelled
|
||||
callback.onCancelled()
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class LifecycleBridgeHandler(
|
||||
*/
|
||||
private fun dismiss(): JsonElement? {
|
||||
activity.runOnUiThread {
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
activity.setResult(SelfVerificationActivity.RESULT_CODE_CANCELLED)
|
||||
activity.finish()
|
||||
}
|
||||
return null
|
||||
@@ -74,19 +74,19 @@ class LifecycleBridgeHandler(
|
||||
if (type != null) {
|
||||
// Flat lifecycle payload (e.g. { type: "proofRequested" }) — treat as success
|
||||
intent.putExtra(SelfVerificationActivity.EXTRA_RESULT_TYPE, type)
|
||||
activity.setResult(Activity.RESULT_OK, intent)
|
||||
activity.setResult(SelfVerificationActivity.RESULT_CODE_SUCCESS, intent)
|
||||
} else if (success && data != null) {
|
||||
// Success result
|
||||
intent.putExtra(SelfVerificationActivity.EXTRA_RESULT_DATA, data)
|
||||
activity.setResult(Activity.RESULT_OK, intent)
|
||||
activity.setResult(SelfVerificationActivity.RESULT_CODE_SUCCESS, intent)
|
||||
} else if (!success && errorCode != null) {
|
||||
// Error result
|
||||
intent.putExtra(SelfVerificationActivity.EXTRA_ERROR_CODE, errorCode)
|
||||
intent.putExtra(SelfVerificationActivity.EXTRA_ERROR_MESSAGE, errorMessage ?: "Unknown error")
|
||||
activity.setResult(Activity.RESULT_FIRST_USER, intent)
|
||||
activity.setResult(SelfVerificationActivity.RESULT_CODE_ERROR, intent)
|
||||
} else {
|
||||
// Cancelled or invalid result
|
||||
activity.setResult(Activity.RESULT_CANCELED, intent)
|
||||
activity.setResult(SelfVerificationActivity.RESULT_CODE_CANCELLED, intent)
|
||||
}
|
||||
|
||||
activity.finish()
|
||||
|
||||
@@ -108,7 +108,7 @@ class AndroidWebViewHost(
|
||||
val url = request?.url?.toString() ?: return true
|
||||
val assetHost = "https://appassets.androidplatform.net/"
|
||||
if (url.startsWith(assetHost)) return false
|
||||
if (isDebugMode && url.startsWith("http://10.0.2.2:5173")) return false
|
||||
if (isDebugMode && url.startsWith("http://127.0.0.1:5173")) return false
|
||||
return true // block everything else
|
||||
}
|
||||
|
||||
@@ -128,8 +128,8 @@ class AndroidWebViewHost(
|
||||
// Load appropriate URL based on mode
|
||||
if (isDebugMode) {
|
||||
// Development mode: connect to Vite dev server
|
||||
// Android emulator uses 10.0.2.2 to access host machine's localhost
|
||||
loadUrl("http://10.0.2.2:5173")
|
||||
// With adb reverse, Android devices can use localhost.
|
||||
loadUrl("http://127.0.0.1:5173")
|
||||
} else {
|
||||
// Production mode: load via WebViewAssetLoader.
|
||||
// The custom PathHandler prepends self-wallet/ to all paths,
|
||||
|
||||
@@ -8,6 +8,7 @@ import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import xyz.self.sdk.api.SelfSdkConfig
|
||||
import xyz.self.sdk.api.VerificationRequest
|
||||
import xyz.self.sdk.api.VerificationResult
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
@@ -122,6 +123,22 @@ class ModelSerializationTest {
|
||||
assertEquals(request, decoded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verificationResult_roundtrip() {
|
||||
val result =
|
||||
VerificationResult(
|
||||
success = true,
|
||||
type = "proofGenerated",
|
||||
userId = "user-1",
|
||||
verificationId = "verification-123",
|
||||
proof = "proof-bytes",
|
||||
claims = mapOf("nationality" to "UTO"),
|
||||
)
|
||||
val encoded = json.encodeToString(result)
|
||||
val decoded = json.decodeFromString<VerificationResult>(encoded)
|
||||
assertEquals(result, decoded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selfSdkConfig_defaults() {
|
||||
val config = SelfSdkConfig()
|
||||
|
||||
@@ -44,10 +44,12 @@ actual class SelfSdk private constructor(
|
||||
|
||||
actual companion object {
|
||||
private var instance: SelfSdk? = null
|
||||
private var configuredWith: SelfSdkConfig? = null
|
||||
|
||||
actual fun configure(config: SelfSdkConfig): SelfSdk {
|
||||
if (instance == null) {
|
||||
if (instance == null || configuredWith != config) {
|
||||
instance = SelfSdk(config)
|
||||
configuredWith = config
|
||||
}
|
||||
return instance!!
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@ import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import xyz.self.sdk.api.SelfSdk
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
SelfSdk.bindActivity(this)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
App()
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import xyz.self.testapp.screens.PassportDetailsScreen
|
||||
import xyz.self.testapp.screens.ResultScreen
|
||||
import xyz.self.testapp.screens.SdkLaunchScreen
|
||||
import xyz.self.testapp.theme.SelfTestTheme
|
||||
import xyz.self.testapp.viewmodels.VerificationViewModel
|
||||
|
||||
@@ -22,8 +23,12 @@ fun App() {
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = "passport_details",
|
||||
startDestination = "sdk_launch",
|
||||
) {
|
||||
composable("sdk_launch") {
|
||||
SdkLaunchScreen(navController)
|
||||
}
|
||||
|
||||
composable("passport_details") {
|
||||
PassportDetailsScreen(navController, viewModel)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
package xyz.self.testapp.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import xyz.self.sdk.api.SelfSdk
|
||||
import xyz.self.sdk.api.SelfSdkCallback
|
||||
import xyz.self.sdk.api.SelfSdkConfig
|
||||
import xyz.self.sdk.api.SelfSdkError
|
||||
import xyz.self.sdk.api.VerificationRequest
|
||||
import xyz.self.sdk.api.VerificationResult
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SdkLaunchScreen(navController: NavController) {
|
||||
var userId by remember { mutableStateOf("test-user") }
|
||||
var scope by remember { mutableStateOf("identity") }
|
||||
var callbackStatus by remember { mutableStateOf("Idle") }
|
||||
var callbackPayload by remember { mutableStateOf<String?>(null) }
|
||||
var callbackError by remember { mutableStateOf<SelfSdkError?>(null) }
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val sdk = remember { SelfSdk.configure(SelfSdkConfig(debug = true)) }
|
||||
val json = remember { Json { prettyPrint = true } }
|
||||
|
||||
Scaffold(
|
||||
topBar = { TopAppBar(title = { Text("SDK Public API Test") }) },
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "This button validates SelfSdk.configure(...).launch(...) end-to-end.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = userId,
|
||||
onValueChange = { userId = it },
|
||||
label = { Text("User ID") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = scope,
|
||||
onValueChange = { scope = it },
|
||||
label = { Text("Scope") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
callbackStatus = "Launching verification..."
|
||||
callbackPayload = null
|
||||
callbackError = null
|
||||
|
||||
val request =
|
||||
VerificationRequest(
|
||||
userId = userId.ifBlank { null },
|
||||
scope = scope.ifBlank { null },
|
||||
disclosures = listOf("name", "nationality", "date_of_birth"),
|
||||
)
|
||||
|
||||
sdk.launch(
|
||||
request = request,
|
||||
callback =
|
||||
object : SelfSdkCallback {
|
||||
override fun onSuccess(result: VerificationResult) {
|
||||
coroutineScope.launch {
|
||||
callbackStatus = "Success"
|
||||
callbackError = null
|
||||
callbackPayload =
|
||||
json.encodeToString(
|
||||
VerificationResult.serializer(),
|
||||
result,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(error: SelfSdkError) {
|
||||
coroutineScope.launch {
|
||||
callbackStatus = "Failure"
|
||||
callbackError = error
|
||||
callbackPayload = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancelled() {
|
||||
coroutineScope.launch {
|
||||
callbackStatus = "Cancelled"
|
||||
callbackError = null
|
||||
callbackPayload = null
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Launch Verification")
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = { navController.navigate("passport_details") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Open Manual MRZ/NFC Flow")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Card(
|
||||
colors =
|
||||
CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text("Callback Status: $callbackStatus")
|
||||
if (callbackError != null) {
|
||||
Text("Error Code: ${callbackError?.code}")
|
||||
Text("Error Message: ${callbackError?.message}")
|
||||
}
|
||||
if (callbackPayload != null) {
|
||||
Text(
|
||||
text = callbackPayload!!,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application android:usesCleartextTraffic="true" />
|
||||
</manifest>
|
||||
@@ -3,16 +3,20 @@
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { NativeModules, SafeAreaView, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import {
|
||||
NativeModules,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import { SelfVerification, type SelfSdkError, type VerificationResult } from '@selfxyz/rn-sdk';
|
||||
|
||||
const defaultRequest = {
|
||||
userId: 'rn-test-user',
|
||||
scope: 'rn-sdk-test',
|
||||
disclosures: [],
|
||||
};
|
||||
|
||||
const fallbackMrzScannerModule = {
|
||||
startScanning: async () => ({
|
||||
documentNumber: 'XK0000000',
|
||||
@@ -38,8 +42,6 @@ function ensureMrzScannerModule(): void {
|
||||
typeof legacyScanner?.startScanning === 'function';
|
||||
|
||||
if (!hasScanner) {
|
||||
// Keep camera bridge round-trip testable in this harness when host-native MRZ isn't wired.
|
||||
// Hermes NativeModules host object can reject writes, so this fallback is best-effort.
|
||||
try {
|
||||
nativeModules.SelfMRZScannerModule = fallbackMrzScannerModule;
|
||||
} catch {
|
||||
@@ -50,24 +52,47 @@ function ensureMrzScannerModule(): void {
|
||||
|
||||
ensureMrzScannerModule();
|
||||
|
||||
type CallbackState =
|
||||
| { status: 'Idle' }
|
||||
| { status: 'Launching verification...' }
|
||||
| { status: 'Success'; payload: string }
|
||||
| { status: 'Failure'; code: string; message: string }
|
||||
| { status: 'Cancelled' };
|
||||
|
||||
function App(): React.JSX.Element {
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
const [status, setStatus] = useState('Ready');
|
||||
const [userId, setUserId] = useState('test-user');
|
||||
const [scope, setScope] = useState('identity');
|
||||
const [callback, setCallback] = useState<CallbackState>({ status: 'Idle' });
|
||||
|
||||
const request = useMemo(() => defaultRequest, []);
|
||||
const request = useMemo(
|
||||
() => ({
|
||||
userId: userId || undefined,
|
||||
scope: scope || undefined,
|
||||
disclosures: ['name', 'nationality', 'date_of_birth'],
|
||||
}),
|
||||
[userId, scope],
|
||||
);
|
||||
|
||||
const handleSuccess = (result: VerificationResult) => {
|
||||
setStatus(`Success: ${result.verificationId ?? 'no verificationId'}`);
|
||||
setCallback({
|
||||
status: 'Success',
|
||||
payload: JSON.stringify(result, null, 2),
|
||||
});
|
||||
setIsVerifying(false);
|
||||
};
|
||||
|
||||
const handleFailure = (error: SelfSdkError) => {
|
||||
setStatus(`Failure: ${error.code} - ${error.message}`);
|
||||
setCallback({
|
||||
status: 'Failure',
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
});
|
||||
setIsVerifying(false);
|
||||
};
|
||||
|
||||
const handleCancelled = () => {
|
||||
setStatus('Cancelled');
|
||||
setCallback({ status: 'Cancelled' });
|
||||
setIsVerifying(false);
|
||||
};
|
||||
|
||||
@@ -90,13 +115,57 @@ function App(): React.JSX.Element {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar barStyle="dark-content" />
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>RN SDK Test Harness</Text>
|
||||
<Text style={styles.subtitle}>Status: {status}</Text>
|
||||
<TouchableOpacity style={styles.button} onPress={() => setIsVerifying(true)}>
|
||||
<Text style={styles.buttonText}>Launch Verification</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.topBar}>
|
||||
<Text style={styles.topBarTitle}>SDK Public API Test</Text>
|
||||
</View>
|
||||
<ScrollView contentContainerStyle={styles.content}>
|
||||
<Text style={styles.description}>
|
||||
This button validates SelfSdk.configure(...).launch(...) end-to-end.
|
||||
</Text>
|
||||
|
||||
<Text style={styles.label}>User ID</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={userId}
|
||||
onChangeText={setUserId}
|
||||
placeholder="test-user"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
|
||||
<Text style={styles.label}>Scope</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={scope}
|
||||
onChangeText={setScope}
|
||||
placeholder="identity"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.primaryButton}
|
||||
onPress={() => {
|
||||
setCallback({ status: 'Launching verification...' });
|
||||
setIsVerifying(true);
|
||||
}}
|
||||
>
|
||||
<Text style={styles.primaryButtonText}>Launch Verification</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.callbackCard}>
|
||||
<Text style={styles.callbackLabel}>Callback Status: {callback.status}</Text>
|
||||
{callback.status === 'Failure' && (
|
||||
<>
|
||||
<Text style={styles.callbackDetail}>Error Code: {callback.code}</Text>
|
||||
<Text style={styles.callbackDetail}>Error Message: {callback.message}</Text>
|
||||
</>
|
||||
)}
|
||||
{callback.status === 'Success' && (
|
||||
<Text style={styles.callbackPayload}>{callback.payload}</Text>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
@@ -106,34 +175,74 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
backgroundColor: '#f6f7f8',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 24,
|
||||
gap: 16,
|
||||
topBar: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
backgroundColor: '#f6f7f8',
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
topBarTitle: {
|
||||
fontSize: 22,
|
||||
fontWeight: '700',
|
||||
color: '#111827',
|
||||
},
|
||||
subtitle: {
|
||||
content: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 32,
|
||||
gap: 12,
|
||||
},
|
||||
description: {
|
||||
fontSize: 14,
|
||||
color: '#374151',
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
},
|
||||
button: {
|
||||
label: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
color: '#6b7280',
|
||||
marginBottom: -8,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
color: '#111827',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
primaryButton: {
|
||||
backgroundColor: '#111827',
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 12,
|
||||
paddingVertical: 14,
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonText: {
|
||||
primaryButtonText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
callbackCard: {
|
||||
backgroundColor: '#e8e5f0',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
gap: 6,
|
||||
},
|
||||
callbackLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
color: '#1f2937',
|
||||
},
|
||||
callbackDetail: {
|
||||
fontSize: 13,
|
||||
color: '#374151',
|
||||
},
|
||||
callbackPayload: {
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
color: '#374151',
|
||||
lineHeight: 18,
|
||||
},
|
||||
verificationView: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
@@ -15,14 +15,16 @@ import {
|
||||
spacing,
|
||||
} from '@selfxyz/euclid-web';
|
||||
|
||||
import { useBridge } from '../../providers/BridgeProvider';
|
||||
import { useSelfClient } from '../../providers/SelfClientProvider';
|
||||
|
||||
const GENERIC_SCAN_ERROR_MESSAGE = 'We could not read your document. Please try again.';
|
||||
const CAMERA_UNAVAILABLE_MESSAGE = 'Camera is not available on this device.';
|
||||
const MRZ_INVALID_DATA_ERROR = 'MRZ_INVALID_DATA';
|
||||
|
||||
export const DocumentCameraScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const bridge = useBridge();
|
||||
const { analytics, haptic } = useSelfClient();
|
||||
const { analytics, haptic, camera } = useSelfClient();
|
||||
|
||||
const { countryCode = '', documentType = 'p' } =
|
||||
(location.state as {
|
||||
@@ -32,24 +34,50 @@ export const DocumentCameraScreen: React.FC = () => {
|
||||
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const mountedRef = useRef(true);
|
||||
const scanGenerationRef = useRef(0);
|
||||
const scanInFlightRef = useRef(false);
|
||||
|
||||
const scanPrompt =
|
||||
documentType === 'i' ? 'Scan your ID card' : 'Scan your passport';
|
||||
|
||||
const startMRZScan = useCallback(async () => {
|
||||
if (scanInFlightRef.current) return;
|
||||
scanInFlightRef.current = true;
|
||||
|
||||
const scanGeneration = scanGenerationRef.current + 1;
|
||||
scanGenerationRef.current = scanGeneration;
|
||||
|
||||
setScanning(true);
|
||||
setError(null);
|
||||
analytics.trackEvent('camera_mrz_scan_started', {
|
||||
documentType,
|
||||
countryCode,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await bridge.request<{
|
||||
documentNumber: string;
|
||||
dateOfBirth: string;
|
||||
dateOfExpiry: string;
|
||||
}>('camera', 'scanMRZ', { documentType, countryCode });
|
||||
const available = await camera.isAvailable();
|
||||
if (!mountedRef.current || scanGenerationRef.current !== scanGeneration) {
|
||||
return;
|
||||
}
|
||||
if (!available) {
|
||||
setError(CAMERA_UNAVAILABLE_MESSAGE);
|
||||
analytics.trackEvent('camera_mrz_scan_failed', { errorCode: 'CAMERA_NOT_AVAILABLE' });
|
||||
return;
|
||||
}
|
||||
|
||||
analytics.trackEvent('camera_mrz_scan_started', {
|
||||
documentType,
|
||||
countryCode,
|
||||
});
|
||||
|
||||
const result = await camera.scanMRZ({ documentType, countryCode });
|
||||
if (!mountedRef.current || scanGenerationRef.current !== scanGeneration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const passportNumber = result.documentNumber?.trim() ?? '';
|
||||
const dateOfBirth = result.dateOfBirth?.trim() ?? '';
|
||||
const dateOfExpiry = result.dateOfExpiry?.trim() ?? '';
|
||||
if (!passportNumber || !dateOfBirth || !dateOfExpiry) {
|
||||
throw new Error(MRZ_INVALID_DATA_ERROR);
|
||||
}
|
||||
|
||||
haptic.trigger('success');
|
||||
analytics.trackEvent('camera_mrz_scan_success');
|
||||
@@ -58,25 +86,49 @@ export const DocumentCameraScreen: React.FC = () => {
|
||||
state: {
|
||||
countryCode,
|
||||
documentType,
|
||||
passportNumber: result.documentNumber,
|
||||
dateOfBirth: result.dateOfBirth,
|
||||
dateOfExpiry: result.dateOfExpiry,
|
||||
passportNumber,
|
||||
dateOfBirth,
|
||||
dateOfExpiry,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'MRZ scan failed';
|
||||
setError(message);
|
||||
analytics.trackEvent('camera_mrz_scan_failed', { error: message });
|
||||
if (!mountedRef.current || scanGenerationRef.current !== scanGeneration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bridgeErrorCode =
|
||||
typeof err === 'object' &&
|
||||
err !== null &&
|
||||
'code' in err &&
|
||||
typeof (err as { code?: unknown }).code === 'string'
|
||||
? (err as { code: string }).code
|
||||
: undefined;
|
||||
const errorCode =
|
||||
bridgeErrorCode === MRZ_INVALID_DATA_ERROR ||
|
||||
(err instanceof Error && err.message === MRZ_INVALID_DATA_ERROR)
|
||||
? 'MRZ_INVALID_DATA'
|
||||
: 'MRZ_SCAN_FAILED';
|
||||
setError(GENERIC_SCAN_ERROR_MESSAGE);
|
||||
analytics.trackEvent('camera_mrz_scan_failed', { errorCode });
|
||||
} finally {
|
||||
setScanning(false);
|
||||
scanInFlightRef.current = false;
|
||||
if (mountedRef.current && scanGenerationRef.current === scanGeneration) {
|
||||
setScanning(false);
|
||||
}
|
||||
}
|
||||
}, [bridge, navigate, analytics, haptic, documentType, countryCode]);
|
||||
}, [camera, navigate, analytics, haptic, documentType, countryCode]);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
startMRZScan();
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
scanGenerationRef.current += 1;
|
||||
};
|
||||
}, [startMRZScan]);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
scanGenerationRef.current += 1;
|
||||
analytics.trackEvent('camera_screen_closed');
|
||||
navigate('/');
|
||||
}, [navigate, analytics]);
|
||||
|
||||
@@ -5,13 +5,19 @@
|
||||
import type { WebViewBridge } from '../bridge';
|
||||
|
||||
export interface MrzScanResult {
|
||||
documentNumber: string;
|
||||
dateOfBirth: string;
|
||||
dateOfExpiry: string;
|
||||
documentNumber?: string;
|
||||
dateOfBirth?: string;
|
||||
dateOfExpiry?: string;
|
||||
}
|
||||
|
||||
export interface MrzScanParams {
|
||||
documentType?: string;
|
||||
countryCode?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface BridgeCameraAdapter {
|
||||
scanMRZ(): Promise<MrzScanResult>;
|
||||
scanMRZ(params?: MrzScanParams): Promise<MrzScanResult>;
|
||||
isAvailable(): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -19,10 +25,10 @@ export function bridgeCameraAdapter(
|
||||
bridge: WebViewBridge,
|
||||
): BridgeCameraAdapter {
|
||||
return {
|
||||
async scanMRZ(): Promise<MrzScanResult> {
|
||||
async scanMRZ(params?: MrzScanParams): Promise<MrzScanResult> {
|
||||
// Native handler parses the MRZ JSON string into a JsonElement,
|
||||
// which arrives as an object with documentNumber, dateOfBirth, dateOfExpiry.
|
||||
return bridge.request<MrzScanResult>('camera', 'scanMRZ', {});
|
||||
return bridge.request<MrzScanResult>('camera', 'scanMRZ', params ?? {});
|
||||
},
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
|
||||
@@ -36,4 +36,4 @@ export { bridgeBiometricsAdapter } from './biometrics';
|
||||
export type { BridgeBiometricsAdapter } from './biometrics';
|
||||
|
||||
export { bridgeCameraAdapter } from './camera';
|
||||
export type { BridgeCameraAdapter, MrzScanResult } from './camera';
|
||||
export type { BridgeCameraAdapter, MrzScanParams, MrzScanResult } from './camera';
|
||||
|
||||
29
scripts/kmp-start.sh
Normal file
29
scripts/kmp-start.sh
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PORT=5173
|
||||
|
||||
if command -v adb >/dev/null 2>&1; then
|
||||
adb start-server >/dev/null 2>&1 || true
|
||||
|
||||
device_serials="$(adb devices | awk '$2 == "device" { print $1 }')"
|
||||
if [ -n "${device_serials}" ]; then
|
||||
echo "Setting adb reverse tcp:${PORT} -> tcp:${PORT} for connected Android devices..."
|
||||
while IFS= read -r serial; do
|
||||
[ -z "${serial}" ] && continue
|
||||
if adb -s "${serial}" reverse "tcp:${PORT}" "tcp:${PORT}" >/dev/null 2>&1; then
|
||||
echo " ${serial}: ok"
|
||||
else
|
||||
echo " ${serial}: failed (continuing)"
|
||||
fi
|
||||
done <<< "${device_serials}"
|
||||
echo "Use http://127.0.0.1:${PORT} on Android devices with adb reverse."
|
||||
else
|
||||
echo "No adb devices detected. If using emulator, use http://10.0.2.2:${PORT}."
|
||||
fi
|
||||
else
|
||||
echo "adb not found. Skipping reverse setup."
|
||||
fi
|
||||
|
||||
echo "Starting webview dev server on 0.0.0.0:${PORT}..."
|
||||
exec yarn workspace @selfxyz/webview-app dev --host 0.0.0.0 --port "${PORT}"
|
||||
8
specs/ARCHIVE.md
Normal file
8
specs/ARCHIVE.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Spec Archive
|
||||
|
||||
Append-only log of retired specs. When a spec is fully done and no longer needed for active reference, add a row here.
|
||||
|
||||
For full retirement process, see [SPECS-REORG-PLAN.md](./SPECS-REORG-PLAN.md) placement rule 6.
|
||||
|
||||
| Spec | Retired | Outcome | Key decisions / lessons | Final PR(s) |
|
||||
| ---- | ------- | ------- | ----------------------- | ----------- |
|
||||
88
specs/EUCLID-WEB-CONSOLIDATION.md
Normal file
88
specs/EUCLID-WEB-CONSOLIDATION.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Euclid Web Consolidation Plan (Draft)
|
||||
|
||||
## Status
|
||||
|
||||
Draft and evolving. This document captures the current direction and will be refined as migration work progresses.
|
||||
|
||||
## Goal
|
||||
|
||||
Use `euclid-web` as the single source of truth for new SDK flow screens and business flow logic, while KMP/RN/mobile hosts become thin wrappers around a shared WebView flow.
|
||||
|
||||
## Target Architecture
|
||||
|
||||
1. `euclid-web` owns screen UI and flow orchestration.
|
||||
2. Host apps (KMP demo, RN demo, mobile app) own only:
|
||||
- WebView hosting
|
||||
- bridge wiring
|
||||
- platform permissions/lifecycle
|
||||
- callback handoff to native caller
|
||||
3. SDK surface remains stable while underlying UI/flow logic converges to web.
|
||||
|
||||
## Scope Boundary (Current)
|
||||
|
||||
In scope now:
|
||||
|
||||
- WebView launch reliability
|
||||
- Host-to-web bridge contract
|
||||
- Callback plumbing and smoke verification
|
||||
|
||||
Out of scope now:
|
||||
|
||||
- Full verification journey implementation parity in all hosts
|
||||
- Consolidating/removing demo apps
|
||||
- Reworking non-bridge native UI beyond what is needed for hosting
|
||||
|
||||
## Phased Plan
|
||||
|
||||
### Phase 0: Stabilize Launch (Current PR)
|
||||
|
||||
- Keep changes limited to launch reliability and local dev host setup.
|
||||
- Ensure `kmp:start` reliably starts the web server and supports emulator/device access.
|
||||
- Keep debug-only code removed unless required for ongoing validation.
|
||||
|
||||
### Phase 1: Shared Host Contract (Next PR)
|
||||
|
||||
- Define one shared config contract for URL/env/flags consumed by KMP + RN hosts.
|
||||
- Define one shared event/callback contract (`success`, `error`, `cancel`, optional progress).
|
||||
- Add smoke tests in each host for launch -> callback -> close path.
|
||||
|
||||
### Phase 2: Incremental Screen Migration
|
||||
|
||||
- All new flow screens land in `euclid-web` first.
|
||||
- Remove duplicate native screens from demo apps as each flow area reaches parity.
|
||||
- Keep demos as thin bridge/host harnesses during migration.
|
||||
|
||||
### Phase 3: Consolidate Demo Surface
|
||||
|
||||
- Decide final demo strategy once parity is high:
|
||||
- single primary demo + one bridge harness, or
|
||||
- both retained with explicit ownership and minimal overlap.
|
||||
- De-scope duplicated flow logic from host apps.
|
||||
|
||||
### Phase 4: Mobile App Convergence
|
||||
|
||||
- Mobile app consumes the same `euclid-web` flow path as SDK demos.
|
||||
- Native app keeps only host/platform concerns and app-specific shell concerns.
|
||||
|
||||
## Decision Rules
|
||||
|
||||
- If a change is launch/config/bridge related: belongs in host SDK work.
|
||||
- If a change is flow UI/logic: belongs in `euclid-web`.
|
||||
- If work duplicates flow code across hosts: treat as temporary and track removal.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. What is the long-term canonical demo app for partner validation?
|
||||
2. What is the minimum bridge API required before flow migration accelerates?
|
||||
3. Do we need a compatibility matrix per host (KMP/RN/mobile app) for each migrated flow?
|
||||
4. What release gating is required before removing duplicated native flow screens?
|
||||
|
||||
## Proposed Near-Term PR Slices
|
||||
|
||||
1. Host contract + callback schema standardization.
|
||||
2. RN and KMP smoke tests aligned to the same launch/callback assertions.
|
||||
3. First euclid-web-only flow segment integrated into both hosts.
|
||||
|
||||
## Notes
|
||||
|
||||
This is intentionally incomplete. Add decisions and open items as migration discoveries are made.
|
||||
@@ -65,3 +65,7 @@ These decisions were made during this PR cycle. They are now documented in [SDK-
|
||||
1. **Correctness cleanup** — Adapter consolidation, dynamic proving config, crypto adapter interface gap
|
||||
2. **Publishing** — npm publish rn-sdk, finalize AAR/XCFramework packaging
|
||||
3. **Self Wallet migration** — Wire `SelfVerification` into the main app (Phase 2)
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
- Draft consolidation plan: [EUCLID-WEB-CONSOLIDATION.md](./EUCLID-WEB-CONSOLIDATION.md)
|
||||
|
||||
@@ -1,62 +1,64 @@
|
||||
# Self SDK — Spec System
|
||||
# Specs
|
||||
|
||||
> Table of contents for the spec folder. Start here.
|
||||
> Project-first table of contents for all specs. Start here.
|
||||
|
||||
## What This Is
|
||||
## How Specs Are Organized
|
||||
|
||||
A three-tier spec system designed for parallel AI agent execution:
|
||||
Specs are organized by **project** first (`kmp`, `sdk`, `lottie`, `euclid`), not by document intent.
|
||||
|
||||
1. **Project Overview** — architecture, contracts, cross-workstream dependencies
|
||||
2. **Workstream Overviews** — orientation for each person/scope (what you own, context, status)
|
||||
3. **Implementation Specs** — exact code changes, I/O examples, token-budgeted chunks
|
||||
- Use `specs/projects/<project>/` for project-owned docs.
|
||||
- Use `specs/framework/` for generic spec-writing rules/templates.
|
||||
- Use `specs/shared/` for cross-project handoffs.
|
||||
|
||||
Specs double as AI agent prompts. Written in second person, sized for single context windows, with validation commands after every chunk.
|
||||
## Top-Level Navigation
|
||||
|
||||
## Meta-Framework
|
||||
- `framework/`
|
||||
- `SPEC-GUIDE.md`
|
||||
- `TEMPLATES.md`
|
||||
- `PROJECT-RULES.md`
|
||||
- `PRODUCT-SPEC-ENHANCEMENT-PROMPT.md`
|
||||
|
||||
| File | Purpose | When to Read |
|
||||
| -------------------------------------- | ----------------------------------------------------- | --------------------------------------- |
|
||||
| [SPEC-GUIDE.md](./SPEC-GUIDE.md) | How to write specs (generic, portable to any project) | Before writing or reviewing any spec |
|
||||
| [TEMPLATES.md](./TEMPLATES.md) | Copy-paste templates for all three tiers | When creating a new spec |
|
||||
| [PROJECT-RULES.md](./PROJECT-RULES.md) | Project-specific rules and guardrails | Before starting any implementation work |
|
||||
- `projects/sdk/`
|
||||
- SDK-wide architecture, wave plan, handoff, and workstreams
|
||||
|
||||
## Project-Level Specs
|
||||
- `projects/kmp/`
|
||||
- KMP initiative, architecture, status, and KMP-specific planning
|
||||
|
||||
| File | Purpose | When to Read |
|
||||
| ------------------------------------ | ---------------------------------------------------------------------- | ------------------------------------------ |
|
||||
| [SDK-OVERVIEW.md](./SDK-OVERVIEW.md) | Architecture, bridge protocol, module table, decision matrix, glossary | First. Always. |
|
||||
| [WAVE-PLAN.md](./WAVE-PLAN.md) | Dependency-ordered execution plan for parallel agent work | When planning which chunks to execute next |
|
||||
| [KMP-STATUS.md](./KMP-STATUS.md) | At-a-glance SDK completion percentages (web + KMP) | When you need quick status only |
|
||||
- `projects/lottie/`
|
||||
- Lottie migration/review specs
|
||||
|
||||
## Workstream Specs
|
||||
- `projects/euclid/`
|
||||
- Euclid consolidation specs
|
||||
|
||||
Each workstream has two files: `OVERVIEW.md` (stable orientation) and `SPEC.md` (living implementation details).
|
||||
- `shared/handoffs/`
|
||||
- Cross-project security and transition docs
|
||||
|
||||
| Workstream | Overview | Implementation Spec | Status |
|
||||
| -------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------- | ----------------------------------------------- |
|
||||
| Person 1 — WebView UI + Bridge | [OVERVIEW](./person1-webview/OVERVIEW.md) | [SPEC](./person1-webview/SPEC.md) | 28/29 done, 1 pending (dynamic proving config) |
|
||||
| Person 2 — Native Shells (KMP + Swift) | [OVERVIEW](./person2-native-shells/OVERVIEW.md) | [SPEC](./person2-native-shells/SPEC.md) | 27/28 done, 1 pending (KMP test app validation) |
|
||||
| Person 3 — Integrations | [OVERVIEW](./person3-integrations/OVERVIEW.md) | [MiniPay Spec](./person3-integrations/SPEC-MINIPAY-SAMPLE.md) | 26/26 done |
|
||||
| Person 4 — SDK Core | [OVERVIEW](./person4-sdk-core/OVERVIEW.md) | [SPEC](./person4-sdk-core/SPEC.md) | 23/25 done, 2 pending (adapter dedup, crypto) |
|
||||
| Person 5 — RN SDK | [OVERVIEW](./person5-rn-sdk/OVERVIEW.md) | [SPEC](./person5-rn-sdk/SPEC.md) | 21/23 done, 2 pending (wallet integration, npm) |
|
||||
## Current Canonical Entry Points
|
||||
|
||||
- SDK: `specs/projects/sdk/INDEX.md` (planned)
|
||||
- KMP: `specs/projects/kmp/KMP-SPECS-INDEX.md`
|
||||
- Folder migration: `specs/SPECS-REORG-PLAN.md`
|
||||
|
||||
## Migration Map (Legacy -> Target)
|
||||
|
||||
- `specs/SDK-OVERVIEW.md` -> `specs/projects/sdk/SDK-OVERVIEW.md`
|
||||
- `specs/WAVE-PLAN.md` -> `specs/projects/sdk/WAVE-PLAN.md`
|
||||
- `specs/HANDOFF.md` -> `specs/projects/sdk/HANDOFF.md`
|
||||
|
||||
- `specs/person1-webview/*` -> `specs/projects/sdk/workstreams/webview/*`
|
||||
- `specs/person2-native-shells/*` -> `specs/projects/sdk/workstreams/native-shells/*`
|
||||
- `specs/person3-integrations/*` -> `specs/projects/sdk/workstreams/integrations/*`
|
||||
- `specs/person4-sdk-core/*` -> `specs/projects/sdk/workstreams/sdk-core/*`
|
||||
- `specs/person5-rn-sdk/*` -> `specs/projects/sdk/workstreams/rn-sdk/*`
|
||||
|
||||
- `specs/KMP-STATUS.md` -> `specs/projects/kmp/status/KMP-STATUS.md`
|
||||
- `specs/lottie-dotlottie-migration/REVIEW.md` -> `specs/projects/lottie/REVIEW.md`
|
||||
- `specs/EUCLID-WEB-CONSOLIDATION.md` -> `specs/projects/euclid/EUCLID-WEB-CONSOLIDATION.md`
|
||||
- `specs/handoff-p1-fixes/*` -> `specs/shared/handoffs/p1-fixes/*`
|
||||
|
||||
## Reading Order
|
||||
|
||||
**New to the project?**
|
||||
|
||||
1. This README
|
||||
2. [SDK-OVERVIEW.md](./SDK-OVERVIEW.md) — understand the architecture
|
||||
3. Your workstream's `OVERVIEW.md` — understand what you own
|
||||
4. Your workstream's `SPEC.md` — understand what to build
|
||||
|
||||
**Starting a work session?**
|
||||
|
||||
1. [WAVE-PLAN.md](./WAVE-PLAN.md) — find the next available chunk
|
||||
2. Your workstream's `SPEC.md` — read the chunk, check status
|
||||
3. [PROJECT-RULES.md](./PROJECT-RULES.md) — refresh on guardrails
|
||||
|
||||
**Writing a new spec?**
|
||||
|
||||
1. [SPEC-GUIDE.md](./SPEC-GUIDE.md) — how to write specs
|
||||
2. [TEMPLATES.md](./TEMPLATES.md) — copy-paste the right template
|
||||
3. [PROJECT-RULES.md](./PROJECT-RULES.md) — project-specific constraints
|
||||
1. `specs/README.md`
|
||||
2. Your project index under `specs/projects/<project>/`
|
||||
3. Relevant framework docs in `specs/framework/`
|
||||
4. Project workstream specs
|
||||
|
||||
162
specs/SPECS-REORG-PLAN.md
Normal file
162
specs/SPECS-REORG-PLAN.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# Specs Folder Reorganization Plan (Project-First)
|
||||
|
||||
Last updated: March 5, 2026
|
||||
Owner: SDK/specs maintainers
|
||||
Status: Proposed
|
||||
|
||||
## Decision
|
||||
|
||||
Use a **project-first** structure for `specs/`.
|
||||
|
||||
Why:
|
||||
|
||||
- Work typically starts from project context (`kmp`, `lottie`, `sdk`, `euclid`).
|
||||
- Agents can resolve scope faster with one project root.
|
||||
- Cross-project docs are the minority and can live in shared buckets.
|
||||
|
||||
## Goals
|
||||
|
||||
- Make each project's docs discoverable under one folder.
|
||||
- Keep generic authoring rules separate from project specs.
|
||||
- Reduce mixed flat files and stale links.
|
||||
- Preserve compatibility during migration with path mapping.
|
||||
|
||||
## Target Structure
|
||||
|
||||
```text
|
||||
specs/
|
||||
README.md
|
||||
SPECS-REORG-PLAN.md
|
||||
|
||||
framework/
|
||||
SPEC-GUIDE.md
|
||||
TEMPLATES.md
|
||||
PROJECT-RULES.md
|
||||
PRODUCT-SPEC-ENHANCEMENT-PROMPT.md
|
||||
|
||||
projects/
|
||||
sdk/
|
||||
INDEX.md
|
||||
SDK-OVERVIEW.md
|
||||
WAVE-PLAN.md
|
||||
HANDOFF.md
|
||||
workstreams/
|
||||
webview/
|
||||
native-shells/
|
||||
integrations/
|
||||
sdk-core/
|
||||
rn-sdk/
|
||||
|
||||
kmp/
|
||||
INDEX.md
|
||||
KMP-ARCHITECTURE.md
|
||||
KMP-INITIATIVE.md
|
||||
KMP-REORG-PLAN.md
|
||||
status/
|
||||
KMP-STATUS.md
|
||||
workstreams/
|
||||
|
||||
lottie/
|
||||
INDEX.md
|
||||
REVIEW.md
|
||||
|
||||
euclid/
|
||||
INDEX.md
|
||||
EUCLID-WEB-CONSOLIDATION.md
|
||||
|
||||
shared/
|
||||
handoffs/
|
||||
p1-fixes/
|
||||
SECURITY-HARDENING.md
|
||||
|
||||
archive/
|
||||
ARCHIVE.md # append-only table of retired specs
|
||||
sdk/ # full-text copies of retired SDK specs (optional)
|
||||
kmp/ # full-text copies of retired KMP specs (optional)
|
||||
```
|
||||
|
||||
## Placement Rules
|
||||
|
||||
1. If a spec is mainly about one project, place it under `specs/projects/<project>/`.
|
||||
2. Only generic spec system docs go in `specs/framework/`.
|
||||
3. Cross-project coordination and follow-ups go in `specs/shared/`.
|
||||
4. Every project folder should have an `INDEX.md` as its entrypoint.
|
||||
5. New implementation specs should include: `Owner`, `Status`, `Last updated`, `Validation commands`.
|
||||
6. When a spec is fully done: add a row to `specs/ARCHIVE.md` with outcome + key decisions. Either delete the source files (if the "What Was Built" appendix was added per SPEC-GUIDE) or move them to `specs/archive/<project>/`. Workstream OVERVIEW.md files stay until the workstream itself is retired.
|
||||
|
||||
## Migration Map (Current -> Target)
|
||||
|
||||
Framework:
|
||||
|
||||
- `specs/SPEC-GUIDE.md` -> `specs/framework/SPEC-GUIDE.md`
|
||||
- `specs/TEMPLATES.md` -> `specs/framework/TEMPLATES.md`
|
||||
- `specs/PROJECT-RULES.md` -> `specs/framework/PROJECT-RULES.md`
|
||||
- `specs/PRODUCT-SPEC-ENHANCEMENT-PROMPT.md` -> `specs/framework/PRODUCT-SPEC-ENHANCEMENT-PROMPT.md`
|
||||
|
||||
SDK project:
|
||||
|
||||
- `specs/SDK-OVERVIEW.md` -> `specs/projects/sdk/SDK-OVERVIEW.md`
|
||||
- `specs/WAVE-PLAN.md` -> `specs/projects/sdk/WAVE-PLAN.md`
|
||||
- `specs/HANDOFF.md` -> `specs/projects/sdk/HANDOFF.md`
|
||||
- `specs/person1-webview/*` -> `specs/projects/sdk/workstreams/webview/*`
|
||||
- `specs/person2-native-shells/*` -> `specs/projects/sdk/workstreams/native-shells/*`
|
||||
- `specs/person3-integrations/*` -> `specs/projects/sdk/workstreams/integrations/*`
|
||||
- `specs/person4-sdk-core/*` -> `specs/projects/sdk/workstreams/sdk-core/*`
|
||||
- `specs/person5-rn-sdk/*` -> `specs/projects/sdk/workstreams/rn-sdk/*`
|
||||
|
||||
KMP project:
|
||||
|
||||
- `specs/KMP-STATUS.md` -> `specs/projects/kmp/status/KMP-STATUS.md`
|
||||
- `specs/projects/kmp/KMP-*.md` -> keep under `specs/projects/kmp/`
|
||||
|
||||
Lottie project:
|
||||
|
||||
- `specs/lottie-dotlottie-migration/REVIEW.md` -> `specs/projects/lottie/REVIEW.md`
|
||||
|
||||
Euclid project:
|
||||
|
||||
- `specs/EUCLID-WEB-CONSOLIDATION.md` -> `specs/projects/euclid/EUCLID-WEB-CONSOLIDATION.md`
|
||||
|
||||
Shared:
|
||||
|
||||
- `specs/handoff-p1-fixes/*` -> `specs/shared/handoffs/p1-fixes/*`
|
||||
|
||||
## Rollout Phases
|
||||
|
||||
### Phase 1: Index and Policy
|
||||
|
||||
- Update `specs/README.md` to project-first navigation.
|
||||
- Keep a migration mapping table in `README` during transition.
|
||||
|
||||
### Phase 2: Create Target Dirs
|
||||
|
||||
- Create `framework/`, `projects/*`, `shared/` roots.
|
||||
- Add `INDEX.md` placeholders for `sdk`, `kmp`, `lottie`, `euclid`.
|
||||
|
||||
### Phase 3: Move Project-Level Docs
|
||||
|
||||
- Move `SDK-OVERVIEW`, `WAVE-PLAN`, `HANDOFF`, KMP status, lottie review, euclid consolidation.
|
||||
- Update links in moved docs.
|
||||
|
||||
### Phase 4: Move Workstreams
|
||||
|
||||
- Move `person*` directories to `projects/sdk/workstreams/*`.
|
||||
- Update references from old paths.
|
||||
|
||||
### Phase 5: Cleanup
|
||||
|
||||
- Remove old-path mapping table after link convergence.
|
||||
- Add optional metadata validation script for required headings.
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
- `find specs -maxdepth 5 -type f | sort`
|
||||
- `rg -n "person[1-5]-|KMP-STATUS.md|lottie-dotlottie-migration|EUCLID-WEB-CONSOLIDATION" specs`
|
||||
- `rg -n "\]\(\./" specs` (relative-link sanity)
|
||||
|
||||
## Immediate Next Steps
|
||||
|
||||
1. Create project `INDEX.md` files for `sdk`, `kmp`, `lottie`, `euclid`.
|
||||
2. Move `KMP-STATUS.md` into `projects/kmp/status/`.
|
||||
3. Move `lottie-dotlottie-migration/REVIEW.md` into `projects/lottie/`.
|
||||
4. Move `EUCLID-WEB-CONSOLIDATION.md` into `projects/euclid/`.
|
||||
@@ -32,11 +32,11 @@ The Self Wallet is a monolithic React Native app where all logic, NFC, proving,
|
||||
2. Bridges to native only for hardware/OS capabilities (NFC, camera, biometrics, keychain, lifecycle)
|
||||
3. Provides web-native fallback adapters for everything the browser can handle (documents via IndexedDB, crypto hashing via Web Crypto, analytics via console/fetch)
|
||||
|
||||
| Area | Issue |
|
||||
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `packages/webview-bridge/` | Implemented with current protocol/adapters and validated by tests (63 tests passing). |
|
||||
| `packages/webview-app/` | Screens built, routing works. Missing: biometrics + camera adapter wiring, web fallback adapters not all connected in SelfClientProvider. |
|
||||
| Web fallback adapters | IndexedDB documents adapter, Web Crypto hashing adapter, and console analytics adapter exist in bridge package but need wiring in webview-app. |
|
||||
| Area | Issue |
|
||||
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `packages/webview-bridge/` | Implemented with current protocol/adapters and validated by tests (63 tests passing). |
|
||||
| `packages/webview-app/` | Screens built, routing works. SelfClientProvider wiring complete (biometrics, camera, and web fallback adapters connected). |
|
||||
| Web fallback adapters | IndexedDB documents, Web Crypto hashing, console analytics, navigation, and no-op haptic are wired in webview-app. |
|
||||
|
||||
## Design Principles
|
||||
|
||||
@@ -1331,13 +1331,13 @@ Chunk 1F: Bridge Package (no deps — start here)
|
||||
|
||||
## Completion Status
|
||||
|
||||
| Chunk | Description | Size | Status |
|
||||
| ----- | ------------------------ | ---- | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1F | Bridge Package | L | **Done** — 63 tests pass, bridge package and adapters implemented |
|
||||
| 1B | Onboarding Screens | M | **Done** — all 5 screens render |
|
||||
| 1C | Proving + Result Screens | M | **Done** — screens render, proving wired |
|
||||
| 1D | Remaining Screens | S | **Done** — home, settings, coming-soon render |
|
||||
| 1E | WebView App Shell | M | **In Progress** — providers wired, missing: biometrics adapter, camera wiring, some web fallback adapter connections |
|
||||
| Chunk | Description | Size | Status |
|
||||
| ----- | ------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 1F | Bridge Package | L | **Done** — 63 tests pass, bridge package and adapters implemented |
|
||||
| 1B | Onboarding Screens | M | **Done** — all 5 screens render |
|
||||
| 1C | Proving + Result Screens | M | **Done** — screens render, proving wired |
|
||||
| 1D | Remaining Screens | S | **Done** — home, settings, coming-soon render |
|
||||
| 1E | WebView App Shell | M | **Done** — providers/router/entry wired, biometrics + camera adapters connected, web fallbacks connected, lifecycle.ready() on mount |
|
||||
|
||||
## Validation Plan
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
- [x] iOS Swift providers are implemented and wired (NFC, Biometrics, Lifecycle, WebView host + additional providers)
|
||||
- [x] `SelfSdk.launch()` flow is implemented on iOS
|
||||
- [x] Shared KMP validation baseline captured (`:shared:compileKotlinIosSimulatorArm64` + `:shared:jvmTest` successful)
|
||||
- [ ] KMP test app validation on both platforms remains a follow-up validation task
|
||||
- [x] KMP test app validation on both platforms completed (Android assemble + iOS compile)
|
||||
- [x] Platform asymmetry contract documented and signed off (iOS 9-handler superset vs Android 5-handler core set)
|
||||
- [x] MiniPay sample integration is wired (`SelfSdk.launch()` call path present)
|
||||
|
||||
|
||||
@@ -1694,7 +1694,7 @@ SelfSdkCallback fires on completion/dismissal
|
||||
| `SelfSdkCallback.onSuccess` fires | Integration | Result delivery from WebView through lifecycle handler |
|
||||
| `SelfSdkCallback.onCancelled` fires on dismiss | Integration | Dismiss wiring works correctly |
|
||||
|
||||
**Status: PARTIAL** (Android + iOS implementation paths are present; full cross-platform validation remains)
|
||||
**Status: DONE** (Android common launch signature fixed, test app now exercises `SelfSdk.configure(...).launch(...)`, and validation gates pass)
|
||||
|
||||
---
|
||||
|
||||
@@ -2058,20 +2058,20 @@ Chunk 2A: KMP Setup + Bridge Protocol (no deps -- start here)
|
||||
|
||||
## Completion Status
|
||||
|
||||
| Chunk | Description | Size | Status |
|
||||
| ----- | ------------------------------------------ | ------ | --------------------------------------------------------------------------- |
|
||||
| 2A | KMP Setup + Bridge Protocol | S ~3k | **Done** |
|
||||
| 2B | Android WebView Host | S ~2k | **Done** |
|
||||
| 2C | Android Native Handlers (5 handlers) | L ~12k | **Done** |
|
||||
| 2D | iOS WebView Host + Provider Infrastructure | M ~6k | **Superseded** by 2G-2K (Swift wrapper pattern) |
|
||||
| 2E | iOS Native Handlers (3 handlers) | M ~6k | **Superseded** by 2G-2K (Swift wrapper pattern) |
|
||||
| 2F | SDK Public API + Test App | M ~5k | **Partial** (implementation present; validation/contract alignment pending) |
|
||||
| 2G | Factory Infrastructure | S ~3k | **Done** |
|
||||
| 2H | Biometric Handler (iOS) | S ~2k | **Done** |
|
||||
| 2I | Lifecycle Handler (iOS) | S ~2k | **Done** |
|
||||
| 2J | iOS WebView Host + SelfSdk.launch() | M ~5k | **Done** |
|
||||
| 2K | NFC Handler (iOS) | M ~5k | **Done** |
|
||||
| 2L | Camera MRZ (iOS, Phase 2) | S ~2k | **Skipped** (deferred) |
|
||||
| Chunk | Description | Size | Status |
|
||||
| ----- | ------------------------------------------ | ------ | ----------------------------------------------------------------------------------------- |
|
||||
| 2A | KMP Setup + Bridge Protocol | S ~3k | **Done** |
|
||||
| 2B | Android WebView Host | S ~2k | **Done** |
|
||||
| 2C | Android Native Handlers (5 handlers) | L ~12k | **Done** |
|
||||
| 2D | iOS WebView Host + Provider Infrastructure | M ~6k | **Superseded** by 2G-2K (Swift wrapper pattern) |
|
||||
| 2E | iOS Native Handlers (3 handlers) | M ~6k | **Superseded** by 2G-2K (Swift wrapper pattern) |
|
||||
| 2F | SDK Public API + Test App | M ~5k | **Done** (common Android launch fixed, test app launch screen wired, validation complete) |
|
||||
| 2G | Factory Infrastructure | S ~3k | **Done** |
|
||||
| 2H | Biometric Handler (iOS) | S ~2k | **Done** |
|
||||
| 2I | Lifecycle Handler (iOS) | S ~2k | **Done** |
|
||||
| 2J | iOS WebView Host + SelfSdk.launch() | M ~5k | **Done** |
|
||||
| 2K | NFC Handler (iOS) | M ~5k | **Done** |
|
||||
| 2L | Camera MRZ (iOS, Phase 2) | S ~2k | **Skipped** (deferred) |
|
||||
|
||||
## Validation Plan
|
||||
|
||||
@@ -2161,11 +2161,12 @@ cd packages/self-sdk-swift && swift build
|
||||
|
||||
## Follow-Up (Out of Scope)
|
||||
|
||||
| Item | Discovered during | Suggested spec |
|
||||
| ------------------------------ | ----------------- | --------------------------------------------------------------------------------------------- |
|
||||
| Camera MRZ handler for iOS | Chunk 2L scoping | Phase 2 -- add to this spec when needed |
|
||||
| SecureStorage handler for iOS | Design review | **Decided:** Add `SecureStorageProvider` to factory pattern (see SDK-OVERVIEW canonical rule) |
|
||||
| Crypto signing handler for iOS | Design review | Depends on whether secure enclave signing is needed vs. Web Crypto |
|
||||
| Item | Discovered during | Suggested spec |
|
||||
| -------------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Camera MRZ handler for iOS | Chunk 2L scoping | Phase 2 -- add to this spec when needed |
|
||||
| SecureStorage handler for iOS | Design review | **Decided:** Add `SecureStorageProvider` to factory pattern (see SDK-OVERVIEW canonical rule) |
|
||||
| Crypto signing handler for iOS | Design review | Depends on whether secure enclave signing is needed vs. Web Crypto |
|
||||
| LifecycleBridgeHandler thin-wrapper refactor | PR #1805 review | Both Android and iOS `setResult()` have 4-branch business logic (interpreting `type`/`success`/`errorCode` to decide result codes / callback methods). Per PROJECT-RULES.md rule 22 ("no error mapping in native"), TypeScript should send an explicit `resultCode` or `outcome` field, and the handler should pass it through without interpretation. Touches both platform handlers + bridge protocol. |
|
||||
|
||||
## Spec Deviations
|
||||
|
||||
|
||||
104
specs/projects/kmp/KMP-ARCHITECTURE.md
Normal file
104
specs/projects/kmp/KMP-ARCHITECTURE.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# KMP Architecture
|
||||
|
||||
Last updated: March 5, 2026
|
||||
Owner: KMP program
|
||||
Status: Draft
|
||||
|
||||
## Purpose
|
||||
|
||||
Define the canonical architecture for KMP SDK delivery, runtime behavior, and integration contracts.
|
||||
|
||||
## Scope
|
||||
|
||||
In scope:
|
||||
|
||||
- KMP SDK (`packages/kmp-sdk`)
|
||||
- KMP SDK test app (`packages/kmp-sdk-test-app` target name)
|
||||
- Swift companion package (`packages/self-sdk-swift`)
|
||||
- Bridge contract and handler lifecycle
|
||||
|
||||
Out of scope:
|
||||
|
||||
- Product requirements and roadmap sequencing (see `KMP-INITIATIVE.md`)
|
||||
- Detailed task breakdowns per implementation chunk
|
||||
|
||||
## System Context
|
||||
|
||||
- Host apps launch SDK runtime.
|
||||
- SDK runtime hosts WebView flow and routes bridge messages.
|
||||
- Native handlers execute platform-specific actions.
|
||||
- Results are returned through bridge responses to web layer.
|
||||
|
||||
## Module Boundaries
|
||||
|
||||
1. `kmp-sdk`
|
||||
|
||||
- Public API surface (`configure`, `launch`, callbacks)
|
||||
- Shared bridge models and router
|
||||
- Android and iOS native handler bindings
|
||||
|
||||
2. `self-sdk-swift`
|
||||
|
||||
- iOS-native provider implementations
|
||||
- Factory/config wiring for KMP iOS side
|
||||
|
||||
3. `kmp-sdk-test-app`
|
||||
|
||||
- Integration harness for Android/iOS manual validation
|
||||
- Non-production sample host and verification scenarios
|
||||
|
||||
## Runtime Flow
|
||||
|
||||
1. App initializes SDK config.
|
||||
2. SDK launches WebView host.
|
||||
3. Web layer sends bridge request.
|
||||
4. Router dispatches to native handler.
|
||||
5. Native result/error marshalled to bridge response.
|
||||
6. Web flow proceeds or fails with typed error.
|
||||
|
||||
## Bridge Contract
|
||||
|
||||
Required sections for future expansion:
|
||||
|
||||
- Message envelope format
|
||||
- Request/response typing rules
|
||||
- Error code taxonomy
|
||||
- Timeout/retry semantics
|
||||
- Version compatibility policy
|
||||
|
||||
## Platform Notes
|
||||
|
||||
### Android
|
||||
|
||||
- Activity/webview host lifecycle ownership
|
||||
- NFC/camera/permission handling responsibilities
|
||||
- Threading model expectations
|
||||
|
||||
### iOS
|
||||
|
||||
- Provider delegation into Swift package
|
||||
- View controller presentation ownership
|
||||
- Permission and lifecycle edge cases
|
||||
|
||||
## Security and Privacy
|
||||
|
||||
- Sensitive data handling boundaries
|
||||
- Logging restrictions (no secrets/PII)
|
||||
- Storage and retention rules
|
||||
|
||||
## Validation Matrix
|
||||
|
||||
- Unit: router + handler contracts
|
||||
- Build: android + ios compile targets
|
||||
- Integration: test app verification flows
|
||||
- Device: physical NFC/passport success and failure cases
|
||||
|
||||
## Open Decisions
|
||||
|
||||
- [ ] Decision 1: _TBD_
|
||||
- [ ] Decision 2: _TBD_
|
||||
- [ ] Decision 3: _TBD_
|
||||
|
||||
## Change Log
|
||||
|
||||
- 2026-03-05: Initial architecture skeleton created.
|
||||
96
specs/projects/kmp/KMP-INITIATIVE.md
Normal file
96
specs/projects/kmp/KMP-INITIATIVE.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# KMP Initiative
|
||||
|
||||
Last updated: March 5, 2026
|
||||
Owner: KMP program
|
||||
Status: Draft
|
||||
|
||||
## Problem Statement
|
||||
|
||||
KMP specs and DX entrypoints are currently fragmented across mixed naming and locations, increasing onboarding time and execution errors for contributors and agents.
|
||||
|
||||
## Goals
|
||||
|
||||
- Standardize KMP naming across package folders, commands, and specs.
|
||||
- Consolidate KMP specs under one project-intent hierarchy.
|
||||
- Establish architecture and initiative docs as canonical entrypoints.
|
||||
- Improve agent execution reliability with explicit ownership and validation.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Redesigning non-KMP project spec systems in this initiative.
|
||||
- Reworking product requirements outside KMP scope.
|
||||
- Renaming KMP workspace package names.
|
||||
|
||||
## Deliverables
|
||||
|
||||
1. KMP spec tree under `specs/projects/kmp/`
|
||||
2. Command taxonomy (`kmp:sdk:*`, `kmp:test-app:*`, `kmp:all:*`)
|
||||
3. Test app folder rename to `kmp-sdk-test-app` (workspace package name unchanged)
|
||||
4. Migration map from legacy paths and command aliases
|
||||
5. Agent hygiene fields enforced in KMP workstream specs
|
||||
|
||||
## Milestones
|
||||
|
||||
1. Foundation docs
|
||||
|
||||
- `KMP-SPECS-INDEX.md`
|
||||
- `KMP-ARCHITECTURE.md`
|
||||
- `KMP-INITIATIVE.md`
|
||||
|
||||
2. Command migration
|
||||
|
||||
- Add new namespaced commands
|
||||
- Keep compatibility aliases
|
||||
|
||||
3. Naming migration
|
||||
|
||||
- Folder rename for test app (`packages/kmp-test-app` -> `packages/kmp-sdk-test-app`)
|
||||
- Keep `@selfxyz/kmp-test-app` unchanged
|
||||
- Repo-wide path reference update
|
||||
|
||||
4. Spec migration
|
||||
|
||||
- Move KMP-relevant docs into new hierarchy
|
||||
- Add redirects/mapping
|
||||
|
||||
5. Cleanup
|
||||
|
||||
- Remove deprecated aliases after signoff window
|
||||
|
||||
## Owners
|
||||
|
||||
- Initiative lead: _TBD_
|
||||
- DX/commands: _TBD_
|
||||
- Specs migration: _TBD_
|
||||
- Validation/CI: _TBD_
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Agreement on final package folder naming
|
||||
- Agreement on command namespace policy
|
||||
- Coordination with ongoing KMP implementation work
|
||||
|
||||
## Risks
|
||||
|
||||
1. Stale links and references after migration
|
||||
2. Temporary confusion during alias period
|
||||
3. Spec drift without ownership/date stamping
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- New KMP entrypoint exists and is linked from top-level specs index.
|
||||
- New commands cover all existing KMP workflows.
|
||||
- Legacy references are either migrated or mapped.
|
||||
- KMP specs include owner/dependencies/validation metadata.
|
||||
|
||||
## Rollout Plan
|
||||
|
||||
1. Land docs and command taxonomy.
|
||||
2. Land package folder rename.
|
||||
3. Land spec migration and mapping.
|
||||
4. Remove deprecated aliases after agreed window.
|
||||
|
||||
## Change Log
|
||||
|
||||
- 2026-03-05: Initial initiative skeleton created.
|
||||
- 2026-03-05: Updated naming plan to folder rename only; workspace package name unchanged.
|
||||
190
specs/projects/kmp/KMP-REORG-PLAN.md
Normal file
190
specs/projects/kmp/KMP-REORG-PLAN.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# KMP Specs and DX Reorganization Plan
|
||||
|
||||
Last updated: March 5, 2026
|
||||
Owner: KMP program
|
||||
Status: Proposed
|
||||
|
||||
## Objective
|
||||
|
||||
Reorganize KMP specs and developer experience surface areas so contributors and agents can find the right docs quickly, run the right commands consistently, and execute changes with clear ownership boundaries.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- KMP specs are grouped by project intent under a dedicated KMP project tree.
|
||||
- KMP package naming is consistent (`kmp-sdk`, `kmp-sdk-test-app` folder naming).
|
||||
- Root command surface is explicit, discoverable, and backward-compatible during migration.
|
||||
- Architecture and initiative docs exist and become the canonical KMP entrypoints.
|
||||
- Agents can navigate and execute work with minimal ambiguity.
|
||||
|
||||
## Naming and Structure Standards
|
||||
|
||||
1. Package naming standard
|
||||
|
||||
- Keep SDK package as `kmp-sdk`.
|
||||
- Rename test host app folder from `kmp-test-app` to `kmp-sdk-test-app`.
|
||||
- Keep workspace package name as `@selfxyz/kmp-test-app` (no package rename).
|
||||
|
||||
2. Spec naming standard
|
||||
|
||||
- Use `KMP-` prefix for KMP-wide docs.
|
||||
- Use `KMP-<TRACK>-<TOPIC>.md` for scoped docs (example: `KMP-NATIVE-API.md`).
|
||||
|
||||
3. Canonical KMP spec tree
|
||||
|
||||
- `specs/projects/kmp/README.md`
|
||||
- `specs/projects/kmp/KMP-ARCHITECTURE.md`
|
||||
- `specs/projects/kmp/KMP-INITIATIVE.md`
|
||||
- `specs/projects/kmp/status/KMP-STATUS.md`
|
||||
- `specs/projects/kmp/workstreams/*`
|
||||
- `specs/projects/kmp/KMP-SPECS-INDEX.md`
|
||||
- `specs/projects/kmp/KMP-DECISIONS.md`
|
||||
- `specs/projects/kmp/KMP-CHANGELOG.md`
|
||||
|
||||
## Command Surface Reorganization
|
||||
|
||||
Update root `package.json` KMP scripts into explicit namespaces:
|
||||
|
||||
1. SDK commands
|
||||
|
||||
- `kmp:sdk:build`
|
||||
- `kmp:sdk:test`
|
||||
- `kmp:sdk:lint`
|
||||
- `kmp:sdk:format`
|
||||
- `kmp:sdk:clean`
|
||||
|
||||
2. Test app commands
|
||||
|
||||
- `kmp:test-app:android`
|
||||
- `kmp:test-app:ios`
|
||||
- `kmp:test-app:build`
|
||||
- `kmp:test-app:test`
|
||||
- `kmp:test-app:lint`
|
||||
- `kmp:test-app:format`
|
||||
- `kmp:test-app:clean`
|
||||
|
||||
3. Orchestration commands
|
||||
|
||||
- `kmp:all:check` (lint + test + build)
|
||||
- `kmp:all:clean`
|
||||
- `kmp:all:dev`
|
||||
|
||||
4. Backward compatibility
|
||||
|
||||
- Keep existing `kmp:*` aliases mapped to new commands for 1-2 release cycles.
|
||||
- Add deprecation notes in script descriptions and docs.
|
||||
|
||||
## Required New Docs
|
||||
|
||||
1. `KMP-ARCHITECTURE.md`
|
||||
|
||||
- Module boundaries and ownership
|
||||
- Runtime flow diagrams (Android/iOS)
|
||||
- Bridge contract and handler lifecycle
|
||||
- Integration points with RN SDK/WebView artifacts
|
||||
- Risk areas and validation matrix
|
||||
|
||||
2. `KMP-INITIATIVE.md`
|
||||
|
||||
- Problem statement
|
||||
- Goals and non-goals
|
||||
- Milestones and deliverables
|
||||
- Owners and decision records
|
||||
- Rollout and acceptance criteria
|
||||
|
||||
## Agent-Focused Spec Hygiene
|
||||
|
||||
Add the following to each KMP workstream spec:
|
||||
|
||||
- `Owner`
|
||||
- `Depends On`
|
||||
- `Inputs`
|
||||
- `Outputs`
|
||||
- `Safe-to-edit paths`
|
||||
- `Do-not-edit paths`
|
||||
- `Validation commands`
|
||||
- `Last verified` date
|
||||
|
||||
Add a lightweight validator script to enforce required headings in KMP spec files.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Phase 1: Foundations
|
||||
|
||||
- Create KMP spec project tree under `specs/projects/kmp/`.
|
||||
- Add `KMP-ARCHITECTURE.md` and `KMP-INITIATIVE.md` skeletons.
|
||||
- Publish `KMP-SPECS-INDEX.md` as the KMP entrypoint.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- KMP entry docs exist and are linked from `specs/README.md`.
|
||||
|
||||
### Phase 2: Command Taxonomy
|
||||
|
||||
- Add new namespaced KMP commands to root `package.json`.
|
||||
- Add compatibility aliases from old `kmp:*` commands.
|
||||
- Document command matrix (`task -> command -> expected output`).
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- All existing KMP workflows work through new commands.
|
||||
|
||||
### Phase 3: Package Folder Rename
|
||||
|
||||
- Rename folder `packages/kmp-test-app` to `packages/kmp-sdk-test-app`.
|
||||
- Keep workspace package name as `@selfxyz/kmp-test-app`.
|
||||
- Update all path references in scripts, Gradle settings, docs, and specs.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- No path references remain to `packages/kmp-test-app`.
|
||||
- Workspace package name remains unchanged.
|
||||
|
||||
### Phase 4: Spec Migration
|
||||
|
||||
- Move KMP-relevant specs into `specs/projects/kmp/` buckets.
|
||||
- Add compatibility index in root `specs/README.md` mapping old paths to new paths.
|
||||
- Add `KMP-CHANGELOG.md` and `KMP-DECISIONS.md`.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- KMP spec navigation starts at one path and old references are redirected.
|
||||
|
||||
### Phase 5: Deprecation Cleanup
|
||||
|
||||
- Remove old command aliases after signoff window.
|
||||
- Remove transitional links once references converge.
|
||||
- Run full lint/types/build/tests for impacted workspaces.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- No transitional aliases or duplicate KMP spec locations remain.
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
1. Broken references after rename
|
||||
|
||||
- Mitigation: perform repo-wide search/replace + validation pass before merge.
|
||||
|
||||
2. Temporary confusion during command transition
|
||||
|
||||
- Mitigation: keep aliases and publish command matrix with examples.
|
||||
|
||||
3. Spec drift after migration
|
||||
|
||||
- Mitigation: add `Last verified` and heading-validator checks in CI.
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
- `yarn lint`
|
||||
- `yarn types`
|
||||
- `yarn build`
|
||||
- `yarn test`
|
||||
- `rg -n "packages/kmp-test-app" .`
|
||||
- `rg -n "specs/projects/kmp" specs/README.md`
|
||||
|
||||
## Immediate Next Actions
|
||||
|
||||
1. Create architecture and initiative doc skeletons in `specs/projects/kmp/`.
|
||||
2. Add new root KMP command taxonomy with alias compatibility.
|
||||
3. Prepare and execute package folder rename in one focused PR.
|
||||
4. Migrate KMP specs and add redirect mapping.
|
||||
41
specs/projects/kmp/KMP-SPECS-INDEX.md
Normal file
41
specs/projects/kmp/KMP-SPECS-INDEX.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# KMP Specs Index
|
||||
|
||||
Last updated: March 5, 2026
|
||||
Owner: KMP program
|
||||
Status: Draft
|
||||
|
||||
## Start Here
|
||||
|
||||
1. `KMP-INITIATIVE.md` — goals, scope, milestones.
|
||||
2. `KMP-ARCHITECTURE.md` — technical boundaries and runtime model.
|
||||
3. `KMP-REORG-PLAN.md` — migration phases and execution checklist.
|
||||
|
||||
## Canonical Paths
|
||||
|
||||
- `specs/projects/kmp/KMP-INITIATIVE.md`
|
||||
- `specs/projects/kmp/KMP-ARCHITECTURE.md`
|
||||
- `specs/projects/kmp/KMP-REORG-PLAN.md`
|
||||
- `specs/projects/kmp/status/KMP-STATUS.md` (planned)
|
||||
- `specs/projects/kmp/workstreams/` (planned)
|
||||
- `specs/projects/kmp/KMP-DECISIONS.md` (planned)
|
||||
- `specs/projects/kmp/KMP-CHANGELOG.md` (planned)
|
||||
|
||||
## Working Rules for Agents
|
||||
|
||||
- Prefer canonical KMP paths over legacy `person*` docs for KMP planning.
|
||||
- Record `Last updated` date on each material change.
|
||||
- Include validation commands in implementation-facing specs.
|
||||
- Mark unresolved items in `Open Decisions` sections.
|
||||
|
||||
## Migration Tracking
|
||||
|
||||
- [x] Reorg plan created
|
||||
- [x] Architecture skeleton created
|
||||
- [x] Initiative skeleton created
|
||||
- [ ] Status moved to `specs/projects/kmp/status/KMP-STATUS.md`
|
||||
- [ ] Workstreams reorganized under `specs/projects/kmp/workstreams/`
|
||||
- [ ] Legacy path mapping added to top-level `specs/README.md`
|
||||
|
||||
## Change Log
|
||||
|
||||
- 2026-03-05: Initial KMP spec index skeleton created.
|
||||
Reference in New Issue
Block a user