mirror of
https://github.com/selfxyz/self.git
synced 2026-04-27 03:01:15 -04:00
kmp wrap up handoff work (#1785)
* save wip * finalize pr * pr feedback * update specs. feedback * add handoff security doc
This commit is contained in:
@@ -39,7 +39,7 @@ export const PointsNavBar = (props: NativeStackHeaderProps) => {
|
||||
color={black}
|
||||
fontSize={15}
|
||||
fontWeight="500"
|
||||
fontFamily="DIN OT"
|
||||
fontFamily="DINOT-Medium"
|
||||
textAlign="center"
|
||||
style={{
|
||||
letterSpacing: 0.6,
|
||||
|
||||
@@ -108,8 +108,8 @@ actual class SelfSdk private constructor(
|
||||
) {
|
||||
when (resultCode) {
|
||||
Activity.RESULT_OK -> {
|
||||
// Success
|
||||
val resultDataJson = data?.getStringExtra(SelfVerificationActivity.EXTRA_RESULT_DATA)
|
||||
val resultType = data?.getStringExtra(SelfVerificationActivity.EXTRA_RESULT_TYPE)
|
||||
if (resultDataJson != null) {
|
||||
try {
|
||||
val result = deserializeResult(resultDataJson)
|
||||
@@ -122,6 +122,10 @@ actual class SelfSdk private constructor(
|
||||
),
|
||||
)
|
||||
}
|
||||
} else if (resultType != null) {
|
||||
callback.onSuccess(
|
||||
VerificationResult(success = true, type = resultType),
|
||||
)
|
||||
} else {
|
||||
callback.onFailure(
|
||||
SelfSdkError(
|
||||
|
||||
@@ -11,6 +11,7 @@ import kotlinx.serialization.json.jsonPrimitive
|
||||
import xyz.self.sdk.bridge.BridgeDomain
|
||||
import xyz.self.sdk.bridge.BridgeHandler
|
||||
import xyz.self.sdk.bridge.BridgeHandlerException
|
||||
import xyz.self.sdk.webview.SelfVerificationActivity
|
||||
|
||||
/**
|
||||
* Android implementation of lifecycle bridge handler.
|
||||
@@ -72,16 +73,16 @@ class LifecycleBridgeHandler(
|
||||
|
||||
if (type != null) {
|
||||
// Flat lifecycle payload (e.g. { type: "proofRequested" }) — treat as success
|
||||
intent.putExtra("xyz.self.sdk.RESULT_TYPE", type)
|
||||
intent.putExtra(SelfVerificationActivity.EXTRA_RESULT_TYPE, type)
|
||||
activity.setResult(Activity.RESULT_OK, intent)
|
||||
} else if (success && data != null) {
|
||||
// Success result
|
||||
intent.putExtra("xyz.self.sdk.RESULT_DATA", data)
|
||||
intent.putExtra(SelfVerificationActivity.EXTRA_RESULT_DATA, data)
|
||||
activity.setResult(Activity.RESULT_OK, intent)
|
||||
} else if (!success && errorCode != null) {
|
||||
// Error result
|
||||
intent.putExtra("xyz.self.sdk.ERROR_CODE", errorCode)
|
||||
intent.putExtra("xyz.self.sdk.ERROR_MESSAGE", errorMessage ?: "Unknown error")
|
||||
intent.putExtra(SelfVerificationActivity.EXTRA_ERROR_CODE, errorCode)
|
||||
intent.putExtra(SelfVerificationActivity.EXTRA_ERROR_MESSAGE, errorMessage ?: "Unknown error")
|
||||
activity.setResult(Activity.RESULT_FIRST_USER, intent)
|
||||
} else {
|
||||
// Cancelled or invalid result
|
||||
|
||||
@@ -121,6 +121,7 @@ class SelfVerificationActivity : AppCompatActivity() {
|
||||
|
||||
// Result extras
|
||||
const val EXTRA_RESULT_DATA = "xyz.self.sdk.RESULT_DATA"
|
||||
const val EXTRA_RESULT_TYPE = "xyz.self.sdk.RESULT_TYPE"
|
||||
const val EXTRA_ERROR_CODE = "xyz.self.sdk.ERROR_CODE"
|
||||
const val EXTRA_ERROR_MESSAGE = "xyz.self.sdk.ERROR_MESSAGE"
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
data class VerificationResult(
|
||||
val success: Boolean,
|
||||
val type: String? = null,
|
||||
val userId: String? = null,
|
||||
val verificationId: String? = null,
|
||||
val proof: String? = null,
|
||||
|
||||
@@ -5,11 +5,14 @@
|
||||
package xyz.self.sdk.api
|
||||
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import platform.UIKit.UIApplication
|
||||
import platform.UIKit.UIModalPresentationFullScreen
|
||||
import platform.UIKit.UIViewController
|
||||
import platform.UIKit.UIWindow
|
||||
import platform.UIKit.UIWindowScene
|
||||
import platform.darwin.dispatch_async
|
||||
import platform.darwin.dispatch_get_main_queue
|
||||
import xyz.self.sdk.bridge.MessageRouter
|
||||
import xyz.self.sdk.handlers.AnalyticsBridgeHandler
|
||||
import xyz.self.sdk.handlers.BiometricBridgeHandler
|
||||
@@ -72,9 +75,24 @@ actual class SelfSdk private constructor(
|
||||
|
||||
// Create lifecycle handler with callback and dismiss wiring
|
||||
val lifecycleHandler = LifecycleBridgeHandler()
|
||||
lifecycleHandler.pendingCallback = callback
|
||||
lifecycleHandler.dismissAction = {
|
||||
pendingCallback = null
|
||||
var dismissViewController: UIViewController? = null
|
||||
runBlocking {
|
||||
lifecycleHandler.configure(
|
||||
callback = callback,
|
||||
dismiss = {
|
||||
dispatch_async(dispatch_get_main_queue()) {
|
||||
val viewController = dismissViewController
|
||||
if (viewController == null) {
|
||||
pendingCallback = null
|
||||
return@dispatch_async
|
||||
}
|
||||
|
||||
viewController.dismissViewControllerAnimated(true) {
|
||||
pendingCallback = null
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Register all iOS bridge handlers
|
||||
@@ -91,13 +109,7 @@ actual class SelfSdk private constructor(
|
||||
?: throw IllegalStateException("WebView provider not configured. Call SelfSdkSwift.configure() first.")
|
||||
).getViewController()
|
||||
sdkVC.setModalPresentationStyle(UIModalPresentationFullScreen)
|
||||
|
||||
// Wire up dismiss action to dismiss the VC
|
||||
lifecycleHandler.dismissAction = {
|
||||
sdkVC.dismissViewControllerAnimated(true) {
|
||||
pendingCallback = null
|
||||
}
|
||||
}
|
||||
dismissViewController = sdkVC
|
||||
|
||||
val topVC = findTopViewController()
|
||||
if (topVC == null) {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
package xyz.self.sdk.handlers
|
||||
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
@@ -17,8 +19,24 @@ import xyz.self.sdk.bridge.BridgeHandlerException
|
||||
class LifecycleBridgeHandler : BridgeHandler {
|
||||
override val domain = BridgeDomain.LIFECYCLE
|
||||
|
||||
internal var pendingCallback: SelfSdkCallback? = null
|
||||
internal var dismissAction: (() -> Unit)? = null
|
||||
private val mutex = Mutex()
|
||||
private var pendingCallback: SelfSdkCallback? = null
|
||||
private var dismissAction: (() -> Unit)? = null
|
||||
|
||||
private data class LifecycleState(
|
||||
val callback: SelfSdkCallback?,
|
||||
val dismiss: (() -> Unit)?,
|
||||
)
|
||||
|
||||
internal suspend fun configure(
|
||||
callback: SelfSdkCallback?,
|
||||
dismiss: (() -> Unit)?,
|
||||
) {
|
||||
mutex.withLock {
|
||||
pendingCallback = callback
|
||||
dismissAction = dismiss
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun handle(
|
||||
method: String,
|
||||
@@ -36,14 +54,27 @@ class LifecycleBridgeHandler : BridgeHandler {
|
||||
|
||||
private fun ready(): JsonElement? = null
|
||||
|
||||
private fun dismiss(): JsonElement? {
|
||||
pendingCallback?.onCancelled()
|
||||
pendingCallback = null
|
||||
dismissAction?.invoke()
|
||||
private suspend fun consumeLifecycleState(): LifecycleState =
|
||||
mutex.withLock {
|
||||
val state =
|
||||
LifecycleState(
|
||||
callback = pendingCallback,
|
||||
dismiss = dismissAction,
|
||||
)
|
||||
pendingCallback = null
|
||||
dismissAction = null
|
||||
state
|
||||
}
|
||||
|
||||
private suspend fun dismiss(): JsonElement? {
|
||||
val state = consumeLifecycleState()
|
||||
state.callback?.onCancelled()
|
||||
state.dismiss?.invoke()
|
||||
return null
|
||||
}
|
||||
|
||||
private fun setResult(params: Map<String, JsonElement>): JsonElement? {
|
||||
private suspend fun setResult(params: Map<String, JsonElement>): JsonElement? {
|
||||
val state = consumeLifecycleState()
|
||||
val type = params["type"]?.jsonPrimitive?.content
|
||||
val success = params["success"]?.jsonPrimitive?.content?.toBoolean() ?: false
|
||||
val data = params["data"]?.toString()
|
||||
@@ -51,16 +82,17 @@ class LifecycleBridgeHandler : BridgeHandler {
|
||||
val errorMessage = params["errorMessage"]?.jsonPrimitive?.content
|
||||
|
||||
if (type != null) {
|
||||
// Flat lifecycle payload (e.g. { type: "proofRequested" }) — treat as success
|
||||
pendingCallback?.onSuccess(
|
||||
VerificationResult(success = true),
|
||||
// Flat lifecycle payload is a protocol-level success signal.
|
||||
// `type` communicates what completed (e.g. proofRequested).
|
||||
state.callback?.onSuccess(
|
||||
VerificationResult(success = true, type = type),
|
||||
)
|
||||
} else if (success && data != null) {
|
||||
try {
|
||||
val result = Json.decodeFromString(VerificationResult.serializer(), data)
|
||||
pendingCallback?.onSuccess(result)
|
||||
state.callback?.onSuccess(result)
|
||||
} catch (e: Exception) {
|
||||
pendingCallback?.onFailure(
|
||||
state.callback?.onFailure(
|
||||
SelfSdkError(
|
||||
code = "PARSE_ERROR",
|
||||
message = "Failed to parse verification result: ${e.message}",
|
||||
@@ -68,18 +100,17 @@ class LifecycleBridgeHandler : BridgeHandler {
|
||||
)
|
||||
}
|
||||
} else if (!success && errorCode != null) {
|
||||
pendingCallback?.onFailure(
|
||||
state.callback?.onFailure(
|
||||
SelfSdkError(
|
||||
code = errorCode,
|
||||
message = errorMessage ?: "Unknown error",
|
||||
),
|
||||
)
|
||||
} else {
|
||||
pendingCallback?.onCancelled()
|
||||
state.callback?.onCancelled()
|
||||
}
|
||||
|
||||
pendingCallback = null
|
||||
dismissAction?.invoke()
|
||||
state.dismiss?.invoke()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ Host App
|
||||
├─ LifecycleHandler (init, ready, close, error, success)
|
||||
├─ BiometricHandler (authenticate, isAvailable)
|
||||
├─ KeychainHandler (get, set, remove)
|
||||
├─ NfcHandler (scan, cancelScan, isSupported)
|
||||
└─ CameraHandler (isAvailable, scanMRZ — stub)
|
||||
├─ NfcHandler (scan + APDU exchange, cancelScan, isSupported)
|
||||
└─ CameraHandler (isAvailable, scanMRZ via native module)
|
||||
```
|
||||
|
||||
### File List
|
||||
@@ -32,12 +32,12 @@ Host App
|
||||
| `src/handlers/LifecycleHandler.ts` | ~70 | App lifecycle + verification callbacks |
|
||||
| `src/handlers/BiometricHandler.ts` | ~60 | Biometric auth via react-native-biometrics |
|
||||
| `src/handlers/KeychainHandler.ts` | ~65 | Secure storage via react-native-keychain |
|
||||
| `src/handlers/NfcHandler.ts` | ~130 | NFC tag reading via react-native-nfc-manager |
|
||||
| `src/handlers/CameraHandler.ts` | ~25 | Camera stub (isAvailable, scanMRZ not yet impl) |
|
||||
| `src/handlers/NfcHandler.ts` | ~180 | NFC tag reading + APDU exchange via react-native-nfc-manager |
|
||||
| `src/handlers/CameraHandler.ts` | ~90 | MRZ scanning via native SelfMRZScannerModule / MRZScannerModule |
|
||||
| `src/handlers/index.ts` | ~30 | Handler factory (createHandlers) |
|
||||
| `src/index.ts` | ~5 | Public exports |
|
||||
| **Total source** | **~715** | |
|
||||
| **Tests (8 files)** | **~880** | 59 tests |
|
||||
| **Tests (8 files)** | **~950** | 64 tests |
|
||||
|
||||
### Dependencies
|
||||
|
||||
@@ -85,31 +85,40 @@ into their platform build:
|
||||
|
||||
### NFC Scan Return Shape
|
||||
|
||||
The webview-bridge spec expects `nfc.scan` to return raw APDU response
|
||||
bytes. Our implementation returns a higher-level object:
|
||||
The NFC handler returns tag metadata plus optional APDU exchange results:
|
||||
|
||||
```typescript
|
||||
{ connected: true, tagId: string | null, techType: string, params: {...} }
|
||||
{
|
||||
connected: true,
|
||||
tagId: string | null,
|
||||
techType: string,
|
||||
params: {...},
|
||||
apduResponses?: string[] // hex-encoded responses when apduCommands are provided
|
||||
}
|
||||
```
|
||||
|
||||
**Justification:** `react-native-nfc-manager` provides tag discovery and
|
||||
technology negotiation but does not expose raw APDU transceive at the
|
||||
`getTag()` level without additional low-level calls. The current shape
|
||||
gives the web layer enough information to confirm a tag was found and
|
||||
proceed with the verification flow. When APDU command exchange is needed,
|
||||
a `transceive` method should be added to `NfcHandler` that wraps
|
||||
`NfcManager.transceive()`.
|
||||
When `params.apduCommands` (array of hex strings) is provided, the handler
|
||||
iterates through each command, calls `NfcManager.transceive()`, and returns
|
||||
hex-encoded response bytes in `apduResponses`. Progress events are emitted
|
||||
at `apdu_exchange` (70%) and `apdu_complete` (90%).
|
||||
|
||||
---
|
||||
|
||||
## Deferred Decision
|
||||
## Camera / MRZ Implementation
|
||||
|
||||
**Camera / MRZ scanning** — `CameraHandler.scanMRZ` throws
|
||||
`NOT_IMPLEMENTED`. A full implementation requires choosing a camera
|
||||
library (`react-native-vision-camera` is the modern choice) plus an
|
||||
OCR/MRZ parsing layer. `isAvailable` currently returns `true`
|
||||
unconditionally. This should be wired to a real permission check once
|
||||
a camera library is chosen.
|
||||
`CameraHandler` loads the native MRZ scanner module at init time,
|
||||
checking for `SelfMRZScannerModule` (preferred) or `MRZScannerModule`
|
||||
(fallback) from React Native's `NativeModules`.
|
||||
|
||||
- `isAvailable()` returns whether a native MRZ module was found.
|
||||
- `scanMRZ()` calls `scanner.startScanning()`, normalizes the result
|
||||
(extracts `documentNumber`, `dateOfBirth`, `dateOfExpiry`, plus optional
|
||||
`documentType` and `countryCode`), and throws `MRZ_SCAN_FAILED` on
|
||||
scanner errors or `MRZ_SCAN_INVALID_RESULT` if required fields are missing.
|
||||
- If no native module is present, `scanMRZ()` throws `NOT_AVAILABLE`.
|
||||
|
||||
The host app must provide a native MRZ scanner module (e.g., via
|
||||
`react-native-vision-camera` + OCR) that exposes `startScanning()`.
|
||||
|
||||
---
|
||||
|
||||
@@ -119,7 +128,7 @@ a camera library is chosen.
|
||||
|
||||
```bash
|
||||
cd packages/rn-sdk
|
||||
npx vitest run # 59 tests across 8 files
|
||||
npx vitest run # 64 tests across 8 files
|
||||
```
|
||||
|
||||
### Device Testing Checklist
|
||||
@@ -180,16 +189,15 @@ import { SelfVerification } from '@selfxyz/rn-sdk';
|
||||
| Lifecycle handler | Done | init, ready, close, error, success |
|
||||
| Biometric handler | Done | authenticate, isAvailable via react-native-biometrics |
|
||||
| Keychain handler | Done | get, set, remove via react-native-keychain |
|
||||
| NFC handler | Done | scan, cancelScan, isSupported via react-native-nfc-manager |
|
||||
| NFC handler | Done | scan + APDU exchange, cancelScan, isSupported via react-native-nfc-manager |
|
||||
| iOS asset path | Done | Absolute path via react-native-fs, relative fallback |
|
||||
| Android asset path | Done | `file:///android_asset/` |
|
||||
| Dev server override | Done | `devServerUrl` prop |
|
||||
| Camera / MRZ scan | Stub | `isAvailable` hardcoded true, `scanMRZ` throws NOT_IMPLEMENTED |
|
||||
| Camera / MRZ scan | Done | scanMRZ via native SelfMRZScannerModule with result normalization |
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Camera `scanMRZ` is a stub — needs camera library + OCR (see Deferred Decision)
|
||||
- NFC returns tag metadata, not raw APDU bytes (see Spec Deviation)
|
||||
- `CameraHandler.isAvailable` returns `true` unconditionally
|
||||
- Camera/MRZ requires host app to provide a native MRZ scanner module (`SelfMRZScannerModule` or `MRZScannerModule`)
|
||||
- No retry/reconnect logic for WebView crashes
|
||||
- Asset bundling requires manual platform setup by the host app
|
||||
- Physical-device validation breadth for NFC/APDU and camera across host apps is still limited
|
||||
|
||||
79
packages/rn-sdk/assets/self-wallet/assets/index-BZlxLbn7.js
Normal file
79
packages/rn-sdk/assets/self-wallet/assets/index-BZlxLbn7.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
@font-face{font-family:Advercase-Regular;src:url(../fonts/Advercase-Regular.otf) format("opentype");font-display:swap}@font-face{font-family:DINOT-Bold;src:url(../fonts/DINOT-Bold.otf) format("opentype");font-weight:700;font-display:swap}@font-face{font-family:DINOT-Medium;src:url(../fonts/DINOT-Medium.otf) format("opentype");font-weight:500;font-display:swap}@font-face{font-family:IBMPlexMono-Regular;src:url(../fonts/IBMPlexMono-Regular.otf) format("opentype");font-display:swap}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{height:100%;width:100%;overflow:hidden}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}@keyframes spin{to{transform:rotate(360deg)}}
|
||||
File diff suppressed because one or more lines are too long
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Self</title>
|
||||
<script type="module" crossorigin src="/assets/index-DQoDbr-a.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-c1peh97d.css">
|
||||
<script type="module" crossorigin src="./assets/index-BZlxLbn7.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-VdzGwUkN.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -2,23 +2,68 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { startScanning } = vi.hoisted(() => ({
|
||||
startScanning: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('react-native', () => ({
|
||||
NativeModules: {
|
||||
SelfMRZScannerModule: {
|
||||
startScanning,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import { CameraHandler } from '../handlers/CameraHandler';
|
||||
|
||||
describe('CameraHandler', () => {
|
||||
const handler = new CameraHandler();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('has domain "camera"', () => {
|
||||
expect(handler.domain).toBe('camera');
|
||||
});
|
||||
|
||||
it('isAvailable returns true', async () => {
|
||||
it('isAvailable returns true when native MRZ module is present', async () => {
|
||||
const result = await handler.handle('isAvailable', {});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('scanMRZ throws NOT_IMPLEMENTED', async () => {
|
||||
await expect(handler.handle('scanMRZ', {})).rejects.toThrow('MRZ scan not yet implemented');
|
||||
it('scanMRZ returns normalized MRZ data', async () => {
|
||||
startScanning.mockResolvedValue({
|
||||
data: {
|
||||
documentNumber: 'L898902C3',
|
||||
birthDate: '740812',
|
||||
expiryDate: '120415',
|
||||
documentType: 'P',
|
||||
countryCode: 'UTO',
|
||||
},
|
||||
});
|
||||
|
||||
await expect(handler.handle('scanMRZ', {})).resolves.toEqual({
|
||||
documentNumber: 'L898902C3',
|
||||
dateOfBirth: '740812',
|
||||
dateOfExpiry: '120415',
|
||||
documentType: 'P',
|
||||
countryCode: 'UTO',
|
||||
});
|
||||
});
|
||||
|
||||
it('scanMRZ throws MRZ_SCAN_FAILED when native scanner rejects', async () => {
|
||||
startScanning.mockRejectedValue(new Error('Camera permission denied'));
|
||||
|
||||
try {
|
||||
await handler.handle('scanMRZ', {});
|
||||
expect.unreachable('Should have thrown');
|
||||
} catch (err: unknown) {
|
||||
expect((err as { code: string }).code).toBe('MRZ_SCAN_FAILED');
|
||||
expect((err as Error).message).toBe('MRZ scan failed');
|
||||
}
|
||||
});
|
||||
|
||||
it('unknown method throws METHOD_NOT_FOUND', async () => {
|
||||
|
||||
@@ -18,6 +18,7 @@ function createMockNfc() {
|
||||
start: vi.fn(),
|
||||
requestTechnology: vi.fn(),
|
||||
getTag: vi.fn(),
|
||||
transceive: vi.fn(),
|
||||
cancelTechnologyRequest: vi.fn(),
|
||||
};
|
||||
const tech: NfcTechEnum = {
|
||||
@@ -79,6 +80,7 @@ describe('NfcHandler', () => {
|
||||
(mockNfc.manager.start as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
||||
(mockNfc.manager.requestTechnology as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
||||
(mockNfc.manager.getTag as ReturnType<typeof vi.fn>).mockResolvedValue({ id: 'tag-123' });
|
||||
(mockNfc.manager.transceive as ReturnType<typeof vi.fn>).mockResolvedValue([0x90, 0x00]);
|
||||
(mockNfc.manager.cancelTechnologyRequest as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
@@ -123,6 +125,29 @@ describe('NfcHandler', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns APDU responses when apduCommands are provided', async () => {
|
||||
(mockNfc.manager.transceive as ReturnType<typeof vi.fn>).mockResolvedValueOnce([0x90, 0x00]);
|
||||
(mockNfc.manager.transceive as ReturnType<typeof vi.fn>).mockResolvedValueOnce([0x6A, 0x82]);
|
||||
|
||||
const result = await handler.handle('scan', {
|
||||
apduCommands: ['00A4040007A0000002471001', '00B0000000'],
|
||||
}) as Record<string, unknown>;
|
||||
|
||||
expect(result.apduResponses).toEqual(['9000', '6A82']);
|
||||
expect(mockNfc.manager.transceive).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('throws NFC_APDU_NOT_SUPPORTED when transceive is unavailable', async () => {
|
||||
delete (mockNfc.manager as Partial<NfcManagerModule>).transceive;
|
||||
|
||||
try {
|
||||
await handler.handle('scan', { apduCommands: ['00A4040000'] });
|
||||
expect.unreachable('Should have thrown');
|
||||
} catch (err: unknown) {
|
||||
expect((err as { code: string }).code).toBe('NFC_APDU_NOT_SUPPORTED');
|
||||
}
|
||||
});
|
||||
|
||||
it('throws NFC_SCAN_FAILED on NFC error', async () => {
|
||||
(mockNfc.manager.requestTechnology as ReturnType<typeof vi.fn>).mockRejectedValue(
|
||||
new Error('NFC tag lost'),
|
||||
|
||||
@@ -5,19 +5,98 @@
|
||||
import type { BridgeDomain } from '../bridge/types';
|
||||
import type { BridgeHandler } from '../bridge/types';
|
||||
import { BridgeHandlerError } from '../bridge/types';
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
interface MrzScannerModule {
|
||||
startScanning: () => Promise<unknown>;
|
||||
}
|
||||
|
||||
interface MrzScanData {
|
||||
documentNumber: string;
|
||||
dateOfBirth: string;
|
||||
dateOfExpiry: string;
|
||||
documentType?: string;
|
||||
countryCode?: string;
|
||||
}
|
||||
|
||||
function loadMrzScannerModule(): MrzScannerModule | null {
|
||||
const nativeModules = NativeModules as Record<string, unknown>;
|
||||
const scanner =
|
||||
(nativeModules.SelfMRZScannerModule as MrzScannerModule | undefined) ??
|
||||
(nativeModules.MRZScannerModule as MrzScannerModule | undefined);
|
||||
return scanner ?? null;
|
||||
}
|
||||
|
||||
function normalizeMrzScanResult(result: unknown): MrzScanData {
|
||||
const root = (result ?? {}) as Record<string, unknown>;
|
||||
const payload = (root.data ?? root) as Record<string, unknown>;
|
||||
|
||||
const documentNumber =
|
||||
typeof payload.documentNumber === 'string'
|
||||
? payload.documentNumber
|
||||
: typeof payload.passportNumber === 'string'
|
||||
? payload.passportNumber
|
||||
: '';
|
||||
const dateOfBirth =
|
||||
typeof payload.dateOfBirth === 'string'
|
||||
? payload.dateOfBirth
|
||||
: typeof payload.birthDate === 'string'
|
||||
? payload.birthDate
|
||||
: '';
|
||||
const dateOfExpiry =
|
||||
typeof payload.dateOfExpiry === 'string'
|
||||
? payload.dateOfExpiry
|
||||
: typeof payload.expiryDate === 'string'
|
||||
? payload.expiryDate
|
||||
: '';
|
||||
const documentType = typeof payload.documentType === 'string' ? payload.documentType : undefined;
|
||||
const countryCode = typeof payload.countryCode === 'string' ? payload.countryCode : undefined;
|
||||
|
||||
if (!documentNumber || !dateOfBirth || !dateOfExpiry) {
|
||||
throw new BridgeHandlerError(
|
||||
'MRZ_SCAN_INVALID_RESULT',
|
||||
'MRZ scan returned incomplete data',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
documentNumber,
|
||||
dateOfBirth,
|
||||
dateOfExpiry,
|
||||
documentType,
|
||||
countryCode,
|
||||
};
|
||||
}
|
||||
|
||||
export class CameraHandler implements BridgeHandler {
|
||||
readonly domain: BridgeDomain = 'camera';
|
||||
|
||||
async handle(method: string, _params: Record<string, unknown>): Promise<unknown> {
|
||||
const scanner = loadMrzScannerModule();
|
||||
|
||||
switch (method) {
|
||||
case 'isAvailable':
|
||||
return true;
|
||||
return scanner !== null && typeof scanner.startScanning === 'function';
|
||||
case 'scanMRZ':
|
||||
throw new BridgeHandlerError(
|
||||
'NOT_IMPLEMENTED',
|
||||
'MRZ scan not yet implemented',
|
||||
);
|
||||
if (!scanner || typeof scanner.startScanning !== 'function') {
|
||||
throw new BridgeHandlerError(
|
||||
'NOT_AVAILABLE',
|
||||
'MRZ scanner module is not installed',
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await scanner.startScanning();
|
||||
return normalizeMrzScanResult(result);
|
||||
} catch (err) {
|
||||
if (err instanceof BridgeHandlerError) {
|
||||
throw err;
|
||||
}
|
||||
throw new BridgeHandlerError(
|
||||
'MRZ_SCAN_FAILED',
|
||||
'MRZ scan failed',
|
||||
);
|
||||
}
|
||||
default:
|
||||
throw new BridgeHandlerError('METHOD_NOT_FOUND', `Unknown camera method: ${method}`);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
import type { BridgeDomain } from '../bridge/types';
|
||||
import type { BridgeHandler } from '../bridge/types';
|
||||
import { BridgeHandlerError } from '../bridge/types';
|
||||
@@ -14,6 +12,7 @@ export interface NfcManagerModule {
|
||||
start(): Promise<void>;
|
||||
requestTechnology(tech: string): Promise<void>;
|
||||
getTag(): Promise<{ id?: string } | null>;
|
||||
transceive?(command: number[]): Promise<number[]>;
|
||||
cancelTechnologyRequest(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -27,6 +26,28 @@ interface NfcDeps {
|
||||
tech: NfcTechEnum;
|
||||
}
|
||||
|
||||
function parseApduCommand(hexCommand: string): number[] {
|
||||
const normalized = hexCommand.trim().replace(/\s+/g, '').toUpperCase();
|
||||
if (!/^[0-9A-F]+$/.test(normalized) || normalized.length % 2 !== 0) {
|
||||
throw new BridgeHandlerError(
|
||||
'INVALID_PARAMS',
|
||||
`Invalid APDU hex command: ${hexCommand}`,
|
||||
);
|
||||
}
|
||||
|
||||
const bytes: number[] = [];
|
||||
for (let i = 0; i < normalized.length; i += 2) {
|
||||
bytes.push(Number.parseInt(normalized.slice(i, i + 2), 16));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function toHex(bytes: number[]): string {
|
||||
return bytes
|
||||
.map((value) => value.toString(16).padStart(2, '0').toUpperCase())
|
||||
.join('');
|
||||
}
|
||||
|
||||
function loadNfc(): NfcDeps | undefined {
|
||||
try {
|
||||
const mod = require('react-native-nfc-manager');
|
||||
@@ -83,7 +104,7 @@ export class NfcHandler implements BridgeHandler {
|
||||
await manager.start();
|
||||
this.pushProgress('waiting_for_tag', 10);
|
||||
|
||||
const nfcTech = Platform.OS === 'ios' ? tech.IsoDep : tech.IsoDep;
|
||||
const nfcTech = tech.IsoDep;
|
||||
|
||||
await manager.requestTechnology(nfcTech);
|
||||
this.pushProgress('tag_discovered', 30);
|
||||
@@ -91,13 +112,39 @@ export class NfcHandler implements BridgeHandler {
|
||||
const tag = await manager.getTag();
|
||||
this.pushProgress('connected', 50);
|
||||
|
||||
const apduCommands = Array.isArray(params.apduCommands)
|
||||
? params.apduCommands.filter((entry): entry is string => typeof entry === 'string')
|
||||
: [];
|
||||
let apduResponses: string[] | undefined;
|
||||
if (apduCommands.length > 0) {
|
||||
if (typeof manager.transceive !== 'function') {
|
||||
throw new BridgeHandlerError(
|
||||
'NFC_APDU_NOT_SUPPORTED',
|
||||
'NFC transceive is not supported by the installed nfc manager',
|
||||
);
|
||||
}
|
||||
|
||||
this.pushProgress('apdu_exchange', 70);
|
||||
apduResponses = [];
|
||||
for (const command of apduCommands) {
|
||||
const commandBytes = parseApduCommand(command);
|
||||
const responseBytes = await manager.transceive(commandBytes);
|
||||
apduResponses.push(toHex(responseBytes));
|
||||
}
|
||||
this.pushProgress('apdu_complete', 90);
|
||||
}
|
||||
|
||||
return {
|
||||
connected: true,
|
||||
tagId: tag?.id ?? null,
|
||||
techType: nfcTech,
|
||||
params,
|
||||
apduResponses,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof BridgeHandlerError) {
|
||||
this.pushProgress('error', 0);
|
||||
throw err;
|
||||
}
|
||||
this.pushProgress('error', 0);
|
||||
throw new BridgeHandlerError(
|
||||
'NFC_SCAN_FAILED',
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
@font-face {
|
||||
font-family: 'Advercase';
|
||||
font-family: 'Advercase-Regular';
|
||||
src: url('/fonts/Advercase-Regular.otf') format('opentype');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'DIN OT';
|
||||
font-family: 'DINOT-Bold';
|
||||
src: url('/fonts/DINOT-Bold.otf') format('opentype');
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'DIN OT';
|
||||
font-family: 'DINOT-Medium';
|
||||
src: url('/fonts/DINOT-Medium.otf') format('opentype');
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Mono';
|
||||
font-family: 'IBMPlexMono-Regular';
|
||||
src: url('/fonts/IBMPlexMono-Regular.otf') format('opentype');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
consoleAnalyticsAdapter,
|
||||
bridgeLifecycleAdapter,
|
||||
webNavigationAdapter,
|
||||
bridgeHapticAdapter,
|
||||
noOpHapticAdapter,
|
||||
bridgeBiometricsAdapter,
|
||||
bridgeCameraAdapter,
|
||||
} from '@selfxyz/webview-bridge/adapters';
|
||||
@@ -64,6 +64,7 @@ export const SelfClientProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
|
||||
const adapters = useMemo<SelfClientAdapters>(() => {
|
||||
const lifecycle = bridgeLifecycleAdapter(bridge);
|
||||
|
||||
return {
|
||||
scanner: bridgeNFCScannerAdapter(bridge),
|
||||
crypto: bridgeCryptoAdapter(bridge),
|
||||
@@ -76,7 +77,7 @@ export const SelfClientProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
(path: string) => navigate(path),
|
||||
() => navigate(-1),
|
||||
),
|
||||
haptic: bridgeHapticAdapter(bridge),
|
||||
haptic: noOpHapticAdapter(),
|
||||
biometrics: bridgeBiometricsAdapter(bridge),
|
||||
camera: bridgeCameraAdapter(bridge),
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@ export const DocumentCameraScreen: React.FC = () => {
|
||||
|
||||
try {
|
||||
const result = await bridge.request<{
|
||||
passportNumber: string;
|
||||
documentNumber: string;
|
||||
dateOfBirth: string;
|
||||
dateOfExpiry: string;
|
||||
}>('camera', 'scanMRZ', { documentType, countryCode });
|
||||
@@ -58,7 +58,7 @@ export const DocumentCameraScreen: React.FC = () => {
|
||||
state: {
|
||||
countryCode,
|
||||
documentType,
|
||||
passportNumber: result.passportNumber,
|
||||
passportNumber: result.documentNumber,
|
||||
dateOfBirth: result.dateOfBirth,
|
||||
dateOfExpiry: result.dateOfExpiry,
|
||||
},
|
||||
|
||||
@@ -2,17 +2,128 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { ProofRequestScreen, SelfLogo } from '@selfxyz/euclid-web';
|
||||
|
||||
import { useSelfClient } from '../../providers/SelfClientProvider';
|
||||
|
||||
const DEFAULT_REQUEST_TYPE = 'proofRequested';
|
||||
const ALLOWED_REQUEST_TYPES = new Set([
|
||||
'proofRequested',
|
||||
'documentOwnershipConfirmed',
|
||||
]);
|
||||
const DEFAULT_PROOF_ITEMS = [
|
||||
'Age verification',
|
||||
'Nationality',
|
||||
'Document validity',
|
||||
];
|
||||
|
||||
interface ProvingScreenLocationState {
|
||||
requestType?: string;
|
||||
proofItems?: string[];
|
||||
appName?: string;
|
||||
appEndpoint?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
function titleCaseDisclosure(disclosure: string): string {
|
||||
return disclosure
|
||||
.replace(/[_-]+/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.replace(/\b\w/g, (match) => match.toUpperCase());
|
||||
}
|
||||
|
||||
function normalizeRequestType(value: string | null | undefined): string {
|
||||
if (!value) return DEFAULT_REQUEST_TYPE;
|
||||
return ALLOWED_REQUEST_TYPES.has(value) ? value : DEFAULT_REQUEST_TYPE;
|
||||
}
|
||||
|
||||
function normalizeAppEndpoint(value: string | null | undefined): string {
|
||||
if (!value) return '';
|
||||
|
||||
try {
|
||||
const endpoint = new URL(value);
|
||||
const isHttps = endpoint.protocol === 'https:';
|
||||
const isLocalHttp =
|
||||
endpoint.protocol === 'http:' &&
|
||||
(endpoint.hostname === 'localhost' || endpoint.hostname === '127.0.0.1');
|
||||
|
||||
if (!isHttps && !isLocalHttp) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return endpoint.host;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function parseProofItems(search: string): string[] | null {
|
||||
const params = new URLSearchParams(search);
|
||||
const proofItems = params.get('proofItems');
|
||||
if (proofItems) {
|
||||
const items = proofItems
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
if (items.length > 0) return items;
|
||||
}
|
||||
|
||||
const disclosures = params.get('disclosures');
|
||||
if (disclosures) {
|
||||
const items = disclosures
|
||||
.split(',')
|
||||
.map((item) => titleCaseDisclosure(item))
|
||||
.filter(Boolean);
|
||||
if (items.length > 0) return items;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export const ProvingScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const locationState = (location.state ?? {}) as ProvingScreenLocationState;
|
||||
const { analytics, haptic, lifecycle } = useSelfClient();
|
||||
const [proving, setProving] = useState(false);
|
||||
|
||||
const requestType = useMemo(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
return normalizeRequestType(
|
||||
locationState.requestType ??
|
||||
params.get('resultType'),
|
||||
);
|
||||
}, [location.search, locationState.requestType]);
|
||||
|
||||
const proofItems = useMemo(() => {
|
||||
if (Array.isArray(locationState.proofItems) && locationState.proofItems.length > 0) {
|
||||
return locationState.proofItems;
|
||||
}
|
||||
return parseProofItems(location.search) ?? DEFAULT_PROOF_ITEMS;
|
||||
}, [location.search, locationState.proofItems]);
|
||||
|
||||
const appName = useMemo(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
return locationState.appName ?? params.get('appName') ?? 'Verification';
|
||||
}, [location.search, locationState.appName]);
|
||||
|
||||
const appEndpoint = useMemo(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
return normalizeAppEndpoint(
|
||||
locationState.appEndpoint ?? params.get('appEndpoint'),
|
||||
);
|
||||
}, [location.search, locationState.appEndpoint]);
|
||||
|
||||
const timestamp = useMemo(() => {
|
||||
if (typeof locationState.timestamp === 'number') return locationState.timestamp;
|
||||
const queryTimestamp = new URLSearchParams(location.search).get('timestamp');
|
||||
const parsed = queryTimestamp ? Number(queryTimestamp) : Number.NaN;
|
||||
return Number.isFinite(parsed) ? parsed : Date.now();
|
||||
}, [location.search, locationState.timestamp]);
|
||||
|
||||
const onVerify = useCallback(async () => {
|
||||
haptic.trigger('selection');
|
||||
analytics.trackEvent('prove_verify_pressed');
|
||||
@@ -20,7 +131,7 @@ export const ProvingScreen: React.FC = () => {
|
||||
|
||||
try {
|
||||
await lifecycle.setResult({
|
||||
type: 'proofRequested',
|
||||
type: requestType,
|
||||
});
|
||||
|
||||
navigate('/proving/result', { state: { success: true } });
|
||||
@@ -33,7 +144,7 @@ export const ProvingScreen: React.FC = () => {
|
||||
} finally {
|
||||
setProving(false);
|
||||
}
|
||||
}, [navigate, analytics, haptic, lifecycle]);
|
||||
}, [navigate, analytics, haptic, lifecycle, requestType]);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
@@ -47,14 +158,10 @@ export const ProvingScreen: React.FC = () => {
|
||||
onClose={onCancel}
|
||||
onConfirm={onVerify}
|
||||
appIcon={<SelfLogo size={40} />}
|
||||
appName="Verification"
|
||||
appEndpoint=""
|
||||
timestamp={Date.now()}
|
||||
items={[
|
||||
{ label: 'Age verification' },
|
||||
{ label: 'Nationality' },
|
||||
{ label: 'Document validity' },
|
||||
]}
|
||||
appName={appName}
|
||||
appEndpoint={appEndpoint}
|
||||
timestamp={timestamp}
|
||||
items={proofItems.map((label) => ({ label }))}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,10 +6,11 @@ import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [react()],
|
||||
define: { global: 'globalThis' },
|
||||
build: {
|
||||
target: ['chrome90', 'safari15'],
|
||||
target: ['chrome90', 'safari15.4'],
|
||||
rollupOptions: { output: { manualChunks: undefined } },
|
||||
assetsInlineLimit: 102400,
|
||||
outDir: 'dist',
|
||||
|
||||
@@ -10,6 +10,13 @@ const DOCUMENTS_STORE = 'documents';
|
||||
const CATALOG_STORE = 'catalog';
|
||||
const CATALOG_KEY = 'current';
|
||||
|
||||
function cloneForStorage<T>(value: T): T {
|
||||
if (typeof globalThis.structuredClone === 'function') {
|
||||
return globalThis.structuredClone(value);
|
||||
}
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
function openDB(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
@@ -84,7 +91,7 @@ export function indexedDBDocumentsAdapter(): BridgeDocumentsAdapter {
|
||||
|
||||
async saveDocumentCatalog(catalog: unknown): Promise<void> {
|
||||
const db = await getDB();
|
||||
await txPut(db, CATALOG_STORE, CATALOG_KEY, structuredClone(catalog));
|
||||
await txPut(db, CATALOG_STORE, CATALOG_KEY, cloneForStorage(catalog));
|
||||
},
|
||||
|
||||
async loadDocumentById(id: string): Promise<unknown> {
|
||||
@@ -95,7 +102,7 @@ export function indexedDBDocumentsAdapter(): BridgeDocumentsAdapter {
|
||||
|
||||
async saveDocument(id: string, data: unknown): Promise<void> {
|
||||
const db = await getDB();
|
||||
await txPut(db, DOCUMENTS_STORE, id, structuredClone(data));
|
||||
await txPut(db, DOCUMENTS_STORE, id, cloneForStorage(data));
|
||||
},
|
||||
|
||||
async deleteDocument(id: string): Promise<void> {
|
||||
|
||||
@@ -17,3 +17,9 @@ export function bridgeHapticAdapter(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function noOpHapticAdapter(): BridgeHapticAdapter {
|
||||
return {
|
||||
trigger(): void {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export type { BridgeAnalyticsAdapter } from './analytics';
|
||||
export { consoleAnalyticsAdapter } from './analytics-web';
|
||||
export type { ConsoleAnalyticsOptions } from './analytics-web';
|
||||
|
||||
export { bridgeHapticAdapter } from './haptic';
|
||||
export { bridgeHapticAdapter, noOpHapticAdapter } from './haptic';
|
||||
export type { BridgeHapticAdapter } from './haptic';
|
||||
|
||||
export { webNavigationAdapter } from './navigation';
|
||||
|
||||
163
specs/HANDOFF.md
163
specs/HANDOFF.md
@@ -1,135 +1,70 @@
|
||||
# Handoff: Person 1-2-3 PR Review Reconciliation
|
||||
# SDK Implementation — Follow-Up Tracker
|
||||
|
||||
> Branch: `feat/person1-2-3-implementation`
|
||||
> Review date: 2026-02-19
|
||||
> Scope: Documentation reconciliation against `origin/main..HEAD` and current codebase state.
|
||||
> Branch: `justin/kmp-wrap-up-evi-handoff-work`
|
||||
> Last updated: 2026-02-23
|
||||
> Prior review: 2026-02-19 (`feat/person1-2-3-implementation`)
|
||||
|
||||
## What This PR Delivers
|
||||
## What Was Delivered
|
||||
|
||||
- New bridge + WebView packages are implemented and present:
|
||||
- `@selfxyz/webview-bridge`
|
||||
- `@selfxyz/webview-app`
|
||||
- Native shell expansion is implemented across KMP + Swift:
|
||||
- iOS provider + handler chain is present (beyond original 3-handler plan)
|
||||
- Android handler set remains focused on core native needs
|
||||
- RN shell package is implemented:
|
||||
- `@selfxyz/rn-sdk` with component, router, handlers, asset strategy, tests, and a package handoff doc
|
||||
- Integration sample package is implemented:
|
||||
- `@selfxyz/kmp-minipay-sample` with launch flow and result handling
|
||||
Five new packages, all implemented. Validation status:
|
||||
|
||||
## Package Inventory Added In This Branch
|
||||
| Package | Tests | Status |
|
||||
| ----------------------------- | -------------------- | ------ |
|
||||
| `@selfxyz/webview-bridge` | 63/63 | Done |
|
||||
| `@selfxyz/webview-app` | — (build-verified) | Done |
|
||||
| `@selfxyz/rn-sdk` | 64/64 | Done |
|
||||
| `@selfxyz/self-sdk-swift` | — (compile-verified) | Done |
|
||||
| `@selfxyz/kmp-minipay-sample` | — (scaffold) | Done |
|
||||
|
||||
- `packages/webview-bridge/`
|
||||
- `packages/webview-app/`
|
||||
- `packages/self-sdk-swift/`
|
||||
- `packages/rn-sdk/`
|
||||
- `packages/kmp-minipay-sample/`
|
||||
KMP SDK: `compileKotlinIosSimulatorArm64` + `jvmTest` passing. iOS Maestro launch: 1 test, 0 failures.
|
||||
|
||||
## Person 1 (WebView + Bridge)
|
||||
Chunk completion: 23/30 done, 3 partial, 1 skipped, 2 superseded, 1 deferred. See [WAVE-PLAN.md](./WAVE-PLAN.md) for details.
|
||||
|
||||
### Delivered
|
||||
## Open Follow-Up Items
|
||||
|
||||
- Bridge protocol, adapters, schema, mocks, and tests are implemented in `packages/webview-bridge/`.
|
||||
- WebView app shell/screens/provider wiring are implemented in `packages/webview-app/`.
|
||||
### P1 — Validation Gaps
|
||||
|
||||
### Remaining / Follow-Up
|
||||
| Item | Owner | Context |
|
||||
| ----------------------------------------- | ---------- | ----------------------------------------------------------------------------------- |
|
||||
| Physical-device NFC E2E (Android + iOS) | Person 2/3 | No in-repo evidence of real passport scans through any shell. Highest-priority gap. |
|
||||
| Physical-device camera/MRZ validation | Person 2/5 | RN SDK CameraHandler calls native module but untested on device. |
|
||||
| KMP test app validation on both platforms | Person 2 | Compile-verified only; no runtime validation captured. |
|
||||
| Integration validation in Self Wallet app | Person 5 | `SelfVerification` component not yet wired into Self Wallet. |
|
||||
|
||||
- Correctness gap: fallback wiring consistency in `SelfClientProvider`:
|
||||
- haptic currently bridges native (`bridgeHapticAdapter`) instead of no-op fallback
|
||||
- crypto path is hybrid (`hash` via Web Crypto + `sign` via native bridge)
|
||||
- Dynamic proving request values remain hardcoded and should be sourced/configured by request context.
|
||||
### P2 — Correctness / Consistency
|
||||
|
||||
## Person 2 (Native Shells)
|
||||
| Item | Owner | Context |
|
||||
| ---------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Consolidate duplicated fallback adapters | Person 4 | ~150 LOC duplicated across `webview-bridge/adapters/` and `mobile-sdk-alpha/adapters/browser/` (analytics, documents, haptic). `mobile-sdk-alpha` is canonical owner; bridge copies are transitional. |
|
||||
| Source dynamic proving request values from request context | Person 1 | `ProvingScreen` now accepts params but default values are still hardcoded. Config should flow from `SelfSdk.launch(request)`. |
|
||||
| Expose `generateKey()`/`getPublicKey()` in `BridgeCryptoAdapter` | Person 1/4 | Methods exist in iOS native handler (`CryptoBridgeHandler.kt`) and bridge protocol types (`CryptoMethod`), but the `BridgeCryptoAdapter` interface in `webview-bridge/adapters/crypto.ts` only exposes `hash()` and `sign()`. WebView client code cannot call key management methods. |
|
||||
|
||||
### Delivered
|
||||
### P3 — Publishing / Packaging
|
||||
|
||||
- iOS chain (2G-2K scope) is implemented with handler/provider registration in KMP + Swift package.
|
||||
- Android shell + handlers remain implemented and integrated.
|
||||
| Item | Owner | Context |
|
||||
| ---------------------------------------------- | -------- | --------------------------------------------------------------------------------- |
|
||||
| npm publish `@selfxyz/rn-sdk` | Person 5 | Package is implemented but not published. |
|
||||
| Production artifact builds (AAR + XCFramework) | Person 2 | KMP SDK packaging for distribution not finalized. |
|
||||
| Self Wallet migration to `SelfVerification` | Person 5 | Phase 2 — Self Wallet replaces native verification screens with SDK WebView flow. |
|
||||
|
||||
### Remaining / Follow-Up
|
||||
### Deferred (Phase 2)
|
||||
|
||||
- **iOS lifecycle flat payload bug (P1):** `LifecycleBridgeHandler.kt` requires both `success` and `data` fields to classify a result as success, but WebView sends flat payloads like `{ type: 'proofRequested' }`. All iOS completion callbacks are misclassified as cancellations. See `packages/webview-app/src/screens/proving/ProvingScreen.tsx` and `.../ConfirmIdentificationScreen.tsx` for the call sites.
|
||||
- **iOS lifecycle race condition:** `dismiss()` and `setResult()` in `LifecycleBridgeHandler.kt` share `pendingCallback` and `dismissAction` on `Dispatchers.Default` (thread pool) with no synchronization. Concurrent invocation can double-fire callbacks. Fix: route to `Dispatchers.Main` or protect with `Mutex`.
|
||||
- **iOS dev server port mismatch:** `WebViewProviderImpl.swift` hardcodes `localhost:3000` for debug builds, but Vite dev server runs on `5173` (matching Android). Minor — dev-only.
|
||||
- Public API finalization and validation remain partially open:
|
||||
- cross-platform behavior alignment and explicit platform contract documentation
|
||||
- device-level validation coverage and integration hardening
|
||||
- iOS handler scope expanded beyond initial plan; this needs explicit architectural sign-off.
|
||||
| Item | Chunk | Context |
|
||||
| ---------------------------- | ----- | ---------------------------------------------------------------------------------------------------- |
|
||||
| iOS Camera MRZ Handler (KMP) | 2L | Camera/MRZ on iOS via KMP deferred to Phase 2. RN SDK has its own implementation via native modules. |
|
||||
|
||||
## Person 3 (Integrations)
|
||||
## Resolved Decisions (Reference)
|
||||
|
||||
### Delivered
|
||||
These decisions were made during this PR cycle. They are now documented in [SDK-OVERVIEW.md](./SDK-OVERVIEW.md) and do not need further action:
|
||||
|
||||
- MiniPay sample project scaffold exists and wires verification launch + result flow.
|
||||
|
||||
### Remaining / Follow-Up
|
||||
|
||||
- End-to-end physical device validation is still required (especially NFC path and failure modes).
|
||||
- Integration polish/error handling should be validated against real SDK outcomes, not only scaffolding.
|
||||
|
||||
## Person 4 (SDK Core)
|
||||
|
||||
### Delivered
|
||||
|
||||
- Browser/web fallback adapter implementations are present.
|
||||
- Browser entry and exports are in place and consumed by WebView-oriented clients.
|
||||
|
||||
### Remaining / Follow-Up
|
||||
|
||||
- Ownership consolidation decision is needed for duplicated web fallback adapters:
|
||||
- `packages/webview-bridge/src/adapters/`
|
||||
- `packages/mobile-sdk-alpha/src/adapters/browser/`
|
||||
|
||||
## Person 5 (RN SDK) Reconciliation
|
||||
|
||||
Source reconciled from: `packages/rn-sdk/HANDOFF.md`
|
||||
|
||||
### Implemented (confirmed)
|
||||
|
||||
- `SelfVerification` component and `MessageRouter`
|
||||
- Biometric, keychain, lifecycle handlers
|
||||
- NFC handler
|
||||
- Asset loading paths and `devServerUrl` override
|
||||
- Test coverage for handlers/router/asset-loading behavior
|
||||
|
||||
### Carry-Forward Risks / Gaps
|
||||
|
||||
- **Asset paths break production WebView (P1):** `rn-sdk/assets/self-wallet/index.html` uses absolute paths (`src="/assets/..."`) that resolve to `file:///assets/...` on device instead of the bundle directory. Fix: add `base: './'` to `packages/webview-app/vite.config.ts` and rebuild.
|
||||
- NFC spec deviation:
|
||||
- current return is tag metadata flow, not raw APDU exchange path
|
||||
- Camera/MRZ:
|
||||
- `scanMRZ` is still `NOT_IMPLEMENTED`
|
||||
- Both NFC and Camera/MRZ should be prioritized before broad production rollout.
|
||||
|
||||
## Cross-Workstream Findings
|
||||
|
||||
- Duplicate fallback adapters exist in both bridge and core packages; consolidation is required.
|
||||
- Person 1 fallback wiring is inconsistent with intended web-first model (haptic + crypto behavior).
|
||||
- iOS handler scope expanded (9 handlers) while Android keeps web fallbacks for those domains, creating platform asymmetry that needs explicit decision and documentation.
|
||||
- `WAVE-PLAN` aggregate status values were stale and need replacement with reconciled counts.
|
||||
- **`structuredClone` compat:** `documents-web.ts` calls `structuredClone()` which is unavailable on Safari 15.0–15.3 / iOS 15.0–15.3 WKWebView. Vite build target is `safari15`. Either bump target to `safari15.4` or add a `JSON.parse(JSON.stringify())` fallback.
|
||||
- **Font-family name drift:** `webview-app/src/fonts.css` introduces new font names, but `mobile-sdk-alpha/src/constants/fonts.ts`, `app/tamagui.config.ts`, and `app/web/fonts.css` still reference old names (`Advercase-Regular`, `DINOT-Bold`, etc.). Affected code silently falls back to system fonts.
|
||||
|
||||
## Stale / Descoped / Superseded Items
|
||||
|
||||
- 2D/2E are superseded by 2G-2K implementation path.
|
||||
- 4D remains optional/skipped.
|
||||
- 2L remains deferred (Phase 2).
|
||||
- **Hybrid crypto contract:** `hash()` in WebView, `sign()`/`generateKey()`/`getPublicKey()` native.
|
||||
- **Fallback adapter ownership:** `mobile-sdk-alpha` is canonical; `webview-bridge` copies are transitional.
|
||||
- **Platform asymmetry:** Android = 5-handler normative minimum, iOS = 9-handler compatibility superset. Signed off.
|
||||
- **iOS lifecycle fixes:** Flat payload handling, Mutex synchronization, debug port 5173 — all implemented.
|
||||
|
||||
## Suggested Follow-Up PR Order
|
||||
|
||||
1. **Fix runtime-breaking bugs:**
|
||||
- iOS lifecycle flat payload misclassification (all completions fire as cancellations)
|
||||
- iOS lifecycle race condition (`dismiss`/`setResult` concurrency)
|
||||
- RN SDK absolute asset paths (production WebView load failure)
|
||||
2. **Resolve correctness gaps impacting runtime consistency:**
|
||||
- Person 1 fallback wiring (haptic + crypto) and adapter ownership decision
|
||||
- `structuredClone` Safari 15.0–15.3 compat in `documents-web.ts`
|
||||
3. **Resolve high-risk native capability gaps:**
|
||||
- RN SDK NFC/APDU path and Camera/MRZ implementation decision
|
||||
4. **Resolve platform policy and API consistency:**
|
||||
- iOS/Android handler asymmetry documentation + API contract finalization
|
||||
- Font-family name alignment across webview-app, mobile-sdk-alpha, and app
|
||||
- iOS dev server port alignment (`3000` → `5173`)
|
||||
5. **Complete validation and integration hardening:**
|
||||
- Device E2E coverage for MiniPay and cross-platform verification outcomes
|
||||
1. **Physical-device validation** — NFC E2E on Android + iOS with real passports (unblocks confidence for everything else)
|
||||
2. **Correctness cleanup** — Adapter consolidation, dynamic proving config, crypto adapter interface gap
|
||||
3. **Publishing** — npm publish rn-sdk, finalize AAR/XCFramework packaging
|
||||
4. **Self Wallet migration** — Wire `SelfVerification` into the main app (Phase 2)
|
||||
|
||||
@@ -33,11 +33,11 @@ Each workstream has two files: `OVERVIEW.md` (stable orientation) and `SPEC.md`
|
||||
|
||||
| Workstream | Overview | Implementation Spec | Status |
|
||||
| -------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------- | ----------------------------------------------- |
|
||||
| Person 1 — WebView UI + Bridge | [OVERVIEW](./person1-webview/OVERVIEW.md) | [SPEC](./person1-webview/SPEC.md) | 4/5 chunks done, 1E in progress |
|
||||
| Person 2 — Native Shells (KMP + Swift) | [OVERVIEW](./person2-native-shells/OVERVIEW.md) | [SPEC](./person2-native-shells/SPEC.md) | 3 done, 2 superseded, 1 in progress, 5 pending |
|
||||
| Person 3 — Integrations | [OVERVIEW](./person3-integrations/OVERVIEW.md) | [MiniPay Spec](./person3-integrations/SPEC-MINIPAY-SAMPLE.md) | 0/3 chunks done |
|
||||
| Person 4 — SDK Core | [OVERVIEW](./person4-sdk-core/OVERVIEW.md) | [SPEC](./person4-sdk-core/SPEC.md) | 4/5 active chunks done (4D skipped), 4F pending |
|
||||
| Person 5 — RN SDK | [OVERVIEW](./person5-rn-sdk/OVERVIEW.md) | [SPEC](./person5-rn-sdk/SPEC.md) | 0/4 chunks done |
|
||||
| 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) | 25/26 done, 1 pending (physical-device NFC E2E) |
|
||||
| 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) |
|
||||
|
||||
## Reading Order
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Self SDK — Architecture Specification
|
||||
|
||||
> Last updated: 2026-02-17
|
||||
> Last updated: 2026-02-23
|
||||
> Owner: Self Engineering
|
||||
> Status: Active
|
||||
|
||||
@@ -13,19 +13,19 @@
|
||||
## Status Checklist
|
||||
|
||||
- [x] Architecture finalized (WebView engine + two native shells)
|
||||
- [x] Bridge protocol defined and tested (62 tests pass)
|
||||
- [x] Bridge protocol defined and tested (63 tests pass)
|
||||
- [x] Protocol compatibility policy defined (fail closed on version mismatch)
|
||||
- [x] WebView UI screens built (10 screens, routing works)
|
||||
- [x] WebView engine core working (275+ tests pass, XState proving machine)
|
||||
- [x] Android native shell implemented (5 handlers, WebView host, Activity)
|
||||
- [x] Delete 3 unnecessary Android handlers (documents, analytics, haptic); crypto standalone handler deleted but crypto domain still routed natively for signing/key-gen
|
||||
- [ ] iOS native shell implemented (Swift providers via PR #1762, not yet merged)
|
||||
- [ ] Biometrics bridge adapter (domain defined, no adapter implementation)
|
||||
- [ ] Camera bridge adapter wiring in webview-app
|
||||
- [ ] Web fallback adapters (IndexedDB for docs, Web Crypto for hashing)
|
||||
- [x] iOS native shell implemented (provider-based chain present in repo; merge/publish track separately)
|
||||
- [x] Biometrics bridge adapter wired in webview-app
|
||||
- [x] Camera bridge adapter wiring in webview-app
|
||||
- [x] Web fallback adapters (IndexedDB for docs, Web Crypto for hashing)
|
||||
- [x] Browser entry point with zero RN transitive imports
|
||||
- [ ] RN SDK (`SelfVerification` component — does not exist yet)
|
||||
- [ ] MiniPay sample integration
|
||||
- [x] RN SDK (`SelfVerification` component + handlers) implemented
|
||||
- [x] MiniPay sample integration scaffold + launch/result wiring implemented
|
||||
- [x] Canonical `VerificationResult` contract locked in specs (legacy fields disallowed)
|
||||
- [ ] Dynamic proof request items (currently hardcoded in ProvingScreen)
|
||||
- [ ] MRZ data confirmation screen (PR #1767, not yet merged)
|
||||
@@ -119,16 +119,16 @@
|
||||
|
||||
## Module Table
|
||||
|
||||
| Module | Location | Language | What It Does | Status | % Done | Action Needed |
|
||||
| ----------------------- | ---------------------------- | ---------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **WebView Engine** | `packages/mobile-sdk-alpha/` | TypeScript | Proving machine (XState), stores (Zustand), adapter interfaces, 105 source files | 275+ tests pass, RN adapters built | **80%** | Browser entry point with zero RN imports. Web fallback adapters (IndexedDB, Web Crypto). Finish decoupling core from RN peer deps |
|
||||
| **WebView UI** | `packages/webview-app/` | TypeScript (React) | 10 screens: home, country, ID, camera, NFC, confirm, proving, result, settings, coming-soon | All screens render, routing works, bridge integration wired | **75%** | Biometrics + camera adapter wiring. Dynamic proof request items. Wire SelfClientProvider to web fallback adapters |
|
||||
| **Bridge Protocol** | `packages/webview-bridge/` | TypeScript | JSON messaging, 10 domains, 9 adapters, timeout/error handling, mock transport | 62 tests pass, production-ready protocol | **80%** | Add biometrics adapter (domain defined, no implementation). Web fallback adapters for documents/storage |
|
||||
| **Kotlin Native Shell** | `packages/kmp-sdk/` | Kotlin | Android: 5 handlers + WebView host + Activity. iOS: stubs (Swift providers in PR #1762) | Android fully implemented, iOS stubs | **70%** | iOS: implement via Swift provider pattern |
|
||||
| **Swift Providers** | `packages/self-sdk-swift/` | Swift | iOS native implementations: NFC, biometrics, secure storage, WebView hosting | In PR #1762 (not merged) | **30%** | Merge PR #1762. Complete NFC + biometrics + secure storage + lifecycle providers |
|
||||
| **RN Native Shell** | `packages/rn-sdk/` — **NEW** | React Native | `SelfVerification` WebView wrapper, 5 native handler bridges | Does not exist | **0%** | Create thin wrapper: ~200-300 LOC, same bridge protocol as KMP |
|
||||
| **Shared Utilities** | `common/` | TypeScript | Poseidon, Merkle trees, passport parsing, certificates, 150+ files, 88+ exports | Production, 98% browser-compatible | **95%** | No changes needed. Only 2 files require Node.js (optional) |
|
||||
| **Self Wallet App** | `app/` | React Native (v0.76.9) | Full wallet: documents, NFC, proving, KYC, recovery, settings, Turnkey wallet | Production (v2.9.16) | **N/A** | Test environment for SDK. Eventually migrates to `SelfVerification` |
|
||||
| Module | Location | Language | What It Does | Status | % Done | Action Needed |
|
||||
| ----------------------- | ---------------------------- | ---------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ------- | ------------------------------------------------------------------------------------------------ |
|
||||
| **WebView Engine** | `packages/mobile-sdk-alpha/` | TypeScript | Proving machine (XState), stores (Zustand), adapter interfaces, 105 source files | Browser/RN paths and fallback adapters implemented | **85%** | Consolidate fallback adapter ownership cleanup and finish remaining decoupling from RN peer deps |
|
||||
| **WebView UI** | `packages/webview-app/` | TypeScript (React) | 10 screens: home, country, ID, camera, NFC, confirm, proving, result, settings, coming-soon | All screens render, routing works, bridge integration wired | **85%** | Dynamic proof request items are still hardcoded and need request-context sourcing |
|
||||
| **Bridge Protocol** | `packages/webview-bridge/` | TypeScript | JSON messaging, 10 domains, 9 adapters, timeout/error handling, mock transport | 63+ tests pass, protocol stable | **85%** | Complete adapter de-duplication with engine-owned web fallbacks |
|
||||
| **Kotlin Native Shell** | `packages/kmp-sdk/` | Kotlin | Android: 5 handlers + WebView host + Activity. iOS: provider-backed handler chain | Android and iOS implementations present | **85%** | Complete physical-device validation matrix (NFC success/failure on both platforms) |
|
||||
| **Swift Providers** | `packages/self-sdk-swift/` | Swift | iOS native implementations: NFC, biometrics, secure storage, WebView hosting | Implemented in repo and wired through KMP iOS | **80%** | Final artifact/packaging readiness and physical-device validation |
|
||||
| **RN Native Shell** | `packages/rn-sdk/` — **NEW** | React Native | `SelfVerification` WebView wrapper, 5 native handler bridges | Implemented with tests, asset strategy, and APDU-capable NFC | **85%** | Expand real-device integration validation coverage in host apps |
|
||||
| **Shared Utilities** | `common/` | TypeScript | Poseidon, Merkle trees, passport parsing, certificates, 150+ files, 88+ exports | Production, 98% browser-compatible | **95%** | No changes needed. Only 2 files require Node.js (optional) |
|
||||
| **Self Wallet App** | `app/` | React Native (v0.76.9) | Full wallet: documents, NFC, proving, KYC, recovery, settings, Turnkey wallet | Production (v2.9.16) | **N/A** | Test environment for SDK. Eventually migrates to `SelfVerification` |
|
||||
|
||||
## Decision Matrix
|
||||
|
||||
@@ -165,10 +165,20 @@
|
||||
|
||||
> **Web fallback adapter ownership:** Two packages provide adapters, at different layers:
|
||||
>
|
||||
> - **`mobile-sdk-alpha` (`src/adapters/browser/`)** — Engine-level adapters that satisfy the `Adapters` interface (e.g., `createIndexedDBDocumentsAdapter`, `createWebCryptoAdapter`). These are what `SelfClientProvider` in `webview-app` wires up. **This is the canonical source for web fallback implementations.**
|
||||
> - **`mobile-sdk-alpha` (`src/adapters/browser/`)** — Engine-level adapters that satisfy the `Adapters` interface (e.g., `createIndexedDBDocumentsAdapter`, `createWebCryptoAdapter`). **This is the canonical source for web fallback implementations.**
|
||||
> - **`webview-bridge`** — Bridge-level adapters that translate between the bridge protocol and the engine adapters (e.g., `NfcBridgeAdapter` calls `bridge.request('nfc', 'scan', ...)`). For capabilities that don't need native (documents, crypto hash, analytics), the bridge adapter is a thin pass-through to the engine adapter.
|
||||
>
|
||||
> Rule: if a capability runs entirely in the WebView, the engine adapter in `mobile-sdk-alpha` owns the implementation. The bridge package provides the messaging plumbing, not the business logic.
|
||||
>
|
||||
> **Current transitional state (2026-02-23):** `webview-app` still imports web fallback helpers from `webview-bridge` for some domains. This is accepted short-term, but those helpers must remain behavior-compatible with engine adapters until consolidation is complete.
|
||||
|
||||
### Platform Asymmetry Contract (Signed 2026-02-23)
|
||||
|
||||
- **Normative minimum contract (all shells):** `nfc`, `camera`, `biometrics`, `secureStorage`, `lifecycle`, and native `crypto` methods (`sign`, `generateKey`, `getPublicKey`).
|
||||
- **Android KMP:** Implements the normative minimum (5 handlers + native crypto routing).
|
||||
- **iOS KMP:** Implements a compatibility superset (registers additional `documents`, `analytics`, `haptic`, and `crypto` handlers).
|
||||
- **Sign-off rule:** iOS superset handlers are compatibility shims only; they must not become the authoritative implementation for domains designated as WebView fallbacks.
|
||||
- **Cross-platform invariant:** host app callback semantics and `VerificationResult` contract must be identical regardless of shell or platform.
|
||||
|
||||
## Impact Summary
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Execution Wave Plan
|
||||
|
||||
> Last updated: 2026-02-19
|
||||
> Last updated: 2026-02-23
|
||||
> Project: [SDK-OVERVIEW.md](./SDK-OVERVIEW.md)
|
||||
|
||||
Cross-workstream chunk execution plan for parallel AI agent work via `claude --remote`.
|
||||
@@ -46,14 +46,17 @@ A **wave** is a batch of chunks that can execute in parallel because they have n
|
||||
| **3C** | **Person 3 (Integrations)** | **Polish + Error Handling** | **S** | **Partial** | 3B |
|
||||
| 2L | Person 2 (Native Shells) | Camera MRZ Handler (iOS) | S | Deferred (Phase 2) | 2J |
|
||||
|
||||
**Totals (reconciled 2026-02-19):** 30 chunks — 23 done, 3 partial (1E, 2F, 3C), 1 skipped (4D optional), 2 superseded (2D/2E → 2G-2K), 1 deferred (2L Phase 2).
|
||||
**Totals (reconciled 2026-02-23):** 30 chunks — 23 done, 3 partial (1E, 2F, 3C), 1 skipped (4D optional), 2 superseded (2D/2E → 2G-2K), 1 deferred (2L Phase 2).
|
||||
**Remaining to close:** 4 items (3 partial + 1 deferred).
|
||||
|
||||
## Reconciliation Notes (2026-02-19)
|
||||
## Reconciliation Notes (2026-02-23)
|
||||
|
||||
- This file now reflects post-implementation reconciliation, not just pre-execution planning.
|
||||
- The older aggregate summary (`11 done / 13 pending`) was stale and has been replaced by audited counts.
|
||||
- Partial items are blocked on correctness/validation decisions rather than missing package scaffolding.
|
||||
- Decision/documentation blockers from the 2026-02-19 handoff are now reconciled:
|
||||
- hybrid crypto + fallback ownership decisions are explicit
|
||||
- iOS/Android asymmetry contract is documented and signed off
|
||||
- Remaining partials are now primarily validation/outcome gaps (physical-device NFC E2E), plus residual implementation cleanup (fallback adapter de-duplication + dynamic proving request config).
|
||||
- Key carry-forward risks are tracked in `specs/HANDOFF.md`.
|
||||
|
||||
## Execution Waves
|
||||
|
||||
115
specs/handoff-p1-fixes/SECURITY-HARDENING.md
Normal file
115
specs/handoff-p1-fixes/SECURITY-HARDENING.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# SDK Security Hardening — Follow-up Spec
|
||||
|
||||
> Last updated: 2026-02-25
|
||||
> Source: Bot review feedback on PR #1785 (kmp-wrap-up-evi-handoff-work)
|
||||
> Status: Pending
|
||||
|
||||
## Status Checklist
|
||||
|
||||
| Chunk | Description | Priority | Status |
|
||||
|-------|-------------|----------|--------|
|
||||
| 1 | APDU command allowlisting | High | Not started |
|
||||
| 2 | NFC transceive timeout (iOS) | Medium | Not started |
|
||||
| 3 | Redact sensitive data from error messages | Medium | Not started |
|
||||
| 4 | LifecycleBridgeHandler type+error handling | Low | Not started |
|
||||
| 5 | NFC return payload — minimize PII surface | Low | Not started |
|
||||
| 6 | Person 4 crypto tracking | Low | Not started |
|
||||
|
||||
## Context
|
||||
|
||||
PR #1785 wraps up the KMP/EVI handoff work. Automated reviewers (CodeRabbit, Codex) flagged several security hardening items. The quick fixes were already addressed in the PR's feedback commits. This spec tracks the remaining items that need follow-up work.
|
||||
|
||||
## Chunks
|
||||
|
||||
### Chunk 1: APDU Command Allowlisting
|
||||
|
||||
**Priority:** High
|
||||
**Files:** `packages/rn-sdk/src/handlers/NfcHandler.ts`, `packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/handlers/NfcBridgeHandler.kt`
|
||||
|
||||
`params.apduCommands` from the WebView layer is forwarded directly to `NfcManager.transceive()` with no validation. A compromised or malicious WebView payload could issue arbitrary APDUs against any NFC-capable card in range (payment cards, access cards — not just passports).
|
||||
|
||||
**Required work:**
|
||||
- Define an allowlist of valid APDU command prefixes for eMRTD reading (SELECT, READ BINARY, GET CHALLENGE, EXTERNAL AUTHENTICATE, etc.)
|
||||
- Reject commands that don't match the allowlist before calling `transceive()`
|
||||
- Apply the same validation in both RN and KMP handlers
|
||||
- Add tests for rejected commands
|
||||
|
||||
### Chunk 2: NFC Transceive Timeout (iOS)
|
||||
|
||||
**Priority:** Medium
|
||||
**Files:** `packages/rn-sdk/src/handlers/NfcHandler.ts`
|
||||
|
||||
`react-native-nfc-manager` has no per-call timeout for `transceive()`. On iOS, a stuck chip or broken connection can hang the scan indefinitely with no way to recover.
|
||||
|
||||
**Required work:**
|
||||
- Wrap `transceive()` calls in a `Promise.race` with a configurable timeout (e.g., 10s per command)
|
||||
- On timeout, throw `NFC_TIMEOUT` error and clean up the NFC session
|
||||
- On Android, investigate using `NfcManager.setTimeout()` for native-level timeout
|
||||
- Add tests for timeout behavior
|
||||
|
||||
### Chunk 3: Redact Sensitive Data from Error Messages
|
||||
|
||||
**Priority:** Medium
|
||||
**Files:** `packages/rn-sdk/src/handlers/NfcHandler.ts`
|
||||
|
||||
Line 34 includes the raw hex command in the error message: `Invalid APDU hex command: ${hexCommand}`. For passport-reading flows, APDU command bytes can encode key-derivation material from MRZ data. Any upstream `catch` that logs `err.message` would leak that material.
|
||||
|
||||
**Required work:**
|
||||
- Replace `${hexCommand}` with a truncated/masked version (e.g., first 4 chars + `...`) or remove it entirely
|
||||
- Audit other error paths in NFC/Camera handlers for similar PII leakage
|
||||
|
||||
### Chunk 4: LifecycleBridgeHandler — Type + Error Handling
|
||||
|
||||
**Priority:** Low
|
||||
**Files:** `packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/handlers/LifecycleBridgeHandler.kt`
|
||||
|
||||
When `type != null` (line 84-89), the handler unconditionally creates `VerificationResult(success = true, type = type)` and ignores `success`, `errorCode`, and `errorMessage` params. The current comment says this is intentional ("flat lifecycle payload is a protocol-level success signal"), but if a future caller sends `{ type: "error", success: false, errorCode: "..." }`, error fields would be silently dropped.
|
||||
|
||||
**Required work:**
|
||||
- Decide if `type` should always imply success, or if type+error combinations are valid
|
||||
- If type-only is intentional, add a brief comment or assertion making this explicit
|
||||
- If type+error is valid, update the branching logic to respect `success` when `type` is present
|
||||
|
||||
### Chunk 5: NFC Return Payload — Minimize PII Surface
|
||||
|
||||
**Priority:** Low
|
||||
**Files:** `packages/rn-sdk/src/handlers/NfcHandler.ts`, `packages/rn-sdk/HANDOFF.md`
|
||||
|
||||
The NFC scan returns `tagId` (passport chip UID — a unique persistent identifier, PII under GDPR) and `apduResponses` in hex (can encode raw Data Groups: MRZ text, face image, fingerprints). These pass back to the WebView and risk being logged or persisted inadvertently.
|
||||
|
||||
**Required work:**
|
||||
- Evaluate whether `tagId` is needed by the WebView layer; if not, stop returning it
|
||||
- Add data-handling guidance to HANDOFF.md for `tagId` and `apduResponses`
|
||||
- Also fix HANDOFF.md line 95 which shows `params: {...}` in the NFC return shape but the code no longer returns params
|
||||
|
||||
### Chunk 6: Person 4 Crypto Tracking
|
||||
|
||||
**Priority:** Low
|
||||
**Files:** `specs/person4-sdk-core/`
|
||||
|
||||
The Person 4 workstream has pending crypto work. In a zero-knowledge/passport-verification SDK, partially-wired crypto paths can degrade security guarantees. This needs a tracking issue or explicit deferral decision.
|
||||
|
||||
**Required work:**
|
||||
- Review current Person 4 crypto status
|
||||
- Open a tracked issue if work is outstanding, or add an explicit note that it's deferred
|
||||
|
||||
## Validation
|
||||
|
||||
For chunks 1-3 and 5:
|
||||
```bash
|
||||
cd packages/rn-sdk && npx vitest run
|
||||
```
|
||||
|
||||
For chunk 4:
|
||||
```bash
|
||||
cd packages/kmp-sdk && ./gradlew :shared:jvmTest
|
||||
```
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [ ] Chunk 1: APDU allowlist implemented and tested in both RN and KMP
|
||||
- [ ] Chunk 2: Transceive timeout implemented and tested
|
||||
- [ ] Chunk 3: Sensitive data redacted from all NFC/Camera error messages
|
||||
- [ ] Chunk 4: LifecycleBridgeHandler type+error behavior decided and documented
|
||||
- [ ] Chunk 5: NFC return payload minimized, HANDOFF.md updated
|
||||
- [ ] Chunk 6: Person 4 crypto tracked or explicitly deferred
|
||||
@@ -1,147 +0,0 @@
|
||||
# Plan: Person 1-2-3 PR Review + Handoff
|
||||
|
||||
## Branch
|
||||
|
||||
- `feat/person1-2-3-implementation`
|
||||
|
||||
## Goal
|
||||
|
||||
- Freeze implementation on this branch.
|
||||
- Produce an accurate merge handoff by reconciling spec status vs actual code.
|
||||
- Update planning/checklist docs to reflect reality and isolate follow-up work.
|
||||
- Capture already-established cross-workstream findings so handoff output is actionable, not only procedural.
|
||||
|
||||
## Scope
|
||||
|
||||
- Review-only against current branch changes.
|
||||
- No implementation code changes.
|
||||
- No commits or pushes.
|
||||
|
||||
## Procedure vs Findings
|
||||
|
||||
- This document is both:
|
||||
- A procedural checklist for the review pass.
|
||||
- A findings-aware plan that must carry forward known cross-workstream issues into output artifacts.
|
||||
- If any known finding below cannot be validated during audit, mark it `Unknown (carry forward)` with explicit rationale.
|
||||
|
||||
## Package Inventory Created In This PR
|
||||
|
||||
- `@selfxyz/webview-bridge`
|
||||
- `@selfxyz/webview-app`
|
||||
- `@selfxyz/self-sdk-swift`
|
||||
- `@selfxyz/rn-sdk`
|
||||
- `@selfxyz/kmp-minipay-sample`
|
||||
|
||||
## Known Findings Snapshot (As Of 2026-02-19)
|
||||
|
||||
- Duplicate web fallback adapters exist in both `webview-bridge/src/adapters/` and `packages/mobile-sdk-alpha/src/adapters/browser/` for IndexedDB docs, Web Crypto, console analytics, and no-op haptic; `SelfClientProvider` currently imports from `webview-bridge`. A follow-up consolidation/ownership decision is required.
|
||||
- Person 1 wiring gap: haptic is bridged to native instead of no-op fallback, and crypto adapter behavior is hybrid. This is a correctness follow-up item, not just cleanup.
|
||||
- iOS handler scope expanded beyond original plan (9 handlers shipped vs 3 originally planned), including documents, crypto, analytics, and haptic; Android currently uses web fallbacks for these areas. Platform asymmetry requires an explicit product/architecture decision.
|
||||
- `specs/WAVE-PLAN.md` status counts are stale: current summary text (for example, "11 done / 13 pending") does not match implementation progress; audit should update to a code-evidenced count (currently believed closer to ~23 done / ~4 remaining).
|
||||
- RN SDK highest-risk carry-forward items are the NFC deviation (metadata vs raw APDU path) and camera/MRZ stub (`NOT_IMPLEMENTED`); these should be prioritized in follow-up sequencing.
|
||||
|
||||
## Inputs to Audit
|
||||
|
||||
- `specs/WAVE-PLAN.md`
|
||||
- `specs/person1-webview/OVERVIEW.md`
|
||||
- `specs/person1-webview/SPEC.md`
|
||||
- `specs/person2-native-shells/OVERVIEW.md`
|
||||
- `specs/person2-native-shells/SPEC.md`
|
||||
- `specs/person3-integrations/OVERVIEW.md`
|
||||
- `specs/person3-integrations/SPEC-MINIPAY-SAMPLE.md`
|
||||
- `specs/person4-sdk-core/OVERVIEW.md`
|
||||
- `specs/person4-sdk-core/SPEC.md`
|
||||
- `specs/person5-rn-sdk/OVERVIEW.md`
|
||||
- `specs/person5-rn-sdk/SPEC.md`
|
||||
- `packages/rn-sdk/HANDOFF.md`
|
||||
- `git diff --stat origin/main..HEAD`
|
||||
|
||||
## Audit Method
|
||||
|
||||
1. Build chunk inventory from each spec and overview.
|
||||
2. Reconcile `packages/rn-sdk/HANDOFF.md` claims against current branch changes.
|
||||
3. For RN SDK, validate each item in:
|
||||
|
||||
- `What Is Implemented`
|
||||
- `Known Limitations`
|
||||
- `Spec Deviation`
|
||||
- `Deferred Decision`
|
||||
and map each to one of the status rubric labels with code evidence.
|
||||
|
||||
4. Verify changed files from `origin/main..HEAD` for each claimed chunk.
|
||||
5. Spot-check representative code paths per chunk (not just filenames).
|
||||
6. Classify each chunk with one of:
|
||||
|
||||
- `Done (code present)`
|
||||
- `Partial (code present, validation/integration pending)`
|
||||
- `Pending (not implemented)`
|
||||
- `Superseded/Descoped (stale item)`
|
||||
- `Unknown (carry forward)`
|
||||
|
||||
7. Prefer carry-forward when evidence is ambiguous.
|
||||
8. Ensure Known Findings Snapshot items are either:
|
||||
|
||||
- Confirmed with evidence and reflected in outputs, or
|
||||
- Explicitly marked carry-forward with owner + next PR target.
|
||||
|
||||
## Output Artifacts
|
||||
|
||||
1. Create `specs/HANDOFF.md` with:
|
||||
|
||||
- What This PR Delivers
|
||||
- Remaining Work (Follow-Up PRs)
|
||||
- Person 1 to Person 5 sections
|
||||
- RN SDK reconciliation subsection sourced from `packages/rn-sdk/HANDOFF.md` (implemented, deviations, deferred decisions, limitations)
|
||||
- Cross-workstream findings subsection covering:
|
||||
- Duplicate fallback adapters decision
|
||||
- Person 1 crypto/haptic wiring correction
|
||||
- iOS vs Android handler asymmetry decision
|
||||
- Stale/Descoped Items
|
||||
- Suggested Follow-Up PR Order
|
||||
|
||||
2. Update checklist accuracy in:
|
||||
|
||||
- `specs/person1-webview/OVERVIEW.md`
|
||||
- `specs/person2-native-shells/OVERVIEW.md`
|
||||
- `specs/person3-integrations/OVERVIEW.md`
|
||||
- `specs/person4-sdk-core/OVERVIEW.md`
|
||||
- `specs/person5-rn-sdk/OVERVIEW.md`
|
||||
|
||||
3. Update cross-workstream status in:
|
||||
|
||||
- `specs/WAVE-PLAN.md`
|
||||
- Replace stale aggregate counts with audit-verified totals and date-stamped note.
|
||||
|
||||
## Status Rubric (Important)
|
||||
|
||||
- `Done` requires implementation evidence in this branch.
|
||||
- `Partial` when implementation exists but spec-required runtime validation is not confirmed.
|
||||
- `Pending` when no implementation evidence exists.
|
||||
- `Superseded/Descoped` when work item was replaced by later spec direction.
|
||||
- `Unknown` defaults to carry-forward in `HANDOFF.md`.
|
||||
|
||||
## Editing Rules
|
||||
|
||||
- Documentation-only edits.
|
||||
- Do not alter production/test implementation files.
|
||||
- Preserve historical context, but mark stale statements explicitly.
|
||||
- Prefer explicit dates and branch name in handoff text.
|
||||
|
||||
## Review Checklist Before Finalizing Docs
|
||||
|
||||
- Every changed chunk has a classification.
|
||||
- `packages/rn-sdk/HANDOFF.md` has been reviewed and reconciled with `specs/HANDOFF.md`.
|
||||
- Every RN SDK handoff claim is either confirmed with evidence or explicitly marked carry-forward.
|
||||
- Duplicate fallback adapters are explicitly addressed (decision made or carry-forward item created).
|
||||
- Person 1 crypto/haptic wiring gap is explicitly addressed (fixed status or carry-forward item created).
|
||||
- iOS handler asymmetry is explicitly addressed (decision made or carry-forward item created).
|
||||
- `specs/WAVE-PLAN.md` totals are updated to match audit evidence (with explicit date).
|
||||
- Every stale checklist item is either removed, struck through, or annotated.
|
||||
- `HANDOFF.md` includes concrete follow-up items, not generic placeholders.
|
||||
- Follow-up PR order reflects dependency chain, not calendar preference.
|
||||
|
||||
## Suggested Follow-Up PR Ordering Logic
|
||||
|
||||
1. Close correctness gaps that impact runtime integration first.
|
||||
2. Then complete validation/device coverage.
|
||||
3. Then cleanup/spec debt and optional improvements.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Person 1: WebView UI + Bridge — Workstream Overview
|
||||
|
||||
> Last updated: 2026-02-19
|
||||
> Last updated: 2026-02-23
|
||||
> Owner: Person 1 (WebView UI + Bridge)
|
||||
> Project: [../SDK-OVERVIEW.md](../SDK-OVERVIEW.md)
|
||||
> Implementation: [SPEC.md](./SPEC.md)
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
## Status
|
||||
|
||||
- [x] Bridge protocol types and `WebViewBridge` class (62 tests pass)
|
||||
- [x] Bridge protocol types and `WebViewBridge` class (63 tests pass)
|
||||
- [x] Bridge adapters: NFC, auth, storage, lifecycle, crypto (sign + hash)
|
||||
- [x] Web fallback adapters: IndexedDB documents, Web Crypto, console analytics, navigation, haptic
|
||||
- [x] Mock transport (`MockNativeBridge`) for testing
|
||||
@@ -23,9 +23,8 @@
|
||||
- [x] BridgeProvider and SelfClientProvider wired
|
||||
- [x] Biometrics bridge adapter wired in `SelfClientProvider`
|
||||
- [x] Camera bridge adapter wired in `SelfClientProvider`
|
||||
- [ ] Fallback wiring correctness gap remains:
|
||||
- `haptic` currently uses native bridge trigger instead of web no-op
|
||||
- `crypto` is hybrid (`hash` web, `sign` bridge) and needs explicit contract decision
|
||||
- [x] Fallback wiring reconciled in `SelfClientProvider` (`haptic` uses web no-op)
|
||||
- [x] Hybrid crypto contract signed off (`hash` in WebView, `sign` via native bridge)
|
||||
- [ ] Dynamic proof request items are still hardcoded in `ProvingScreen`
|
||||
|
||||
## What You Own
|
||||
|
||||
@@ -34,7 +34,7 @@ The Self Wallet is a monolithic React Native app where all logic, NFC, proving,
|
||||
|
||||
| Area | Issue |
|
||||
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `packages/webview-bridge/` | Prototype existed but needed rebuild with proper structure. Now mostly done (62 tests). Missing: biometrics adapter, camera adapter wiring. |
|
||||
| `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. |
|
||||
|
||||
@@ -1333,7 +1333,7 @@ Chunk 1F: Bridge Package (no deps — start here)
|
||||
|
||||
| Chunk | Description | Size | Status |
|
||||
| ----- | ------------------------ | ---- | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1F | Bridge Package | L | **Done** — 62 tests pass, all adapters implemented except biometrics |
|
||||
| 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 |
|
||||
@@ -1353,7 +1353,7 @@ ls packages/webview-app/dist/index.html # file must exist
|
||||
# After all chunks:
|
||||
# 1. vite dev serves all 10 routes without console errors
|
||||
# 2. vite build produces dist/ with index.html + bundle
|
||||
# 3. Bridge package: 62+ tests pass
|
||||
# 3. Bridge package: 63+ tests pass
|
||||
# 4. Manual: load dist/index.html in a WebView host (Android/iOS test app)
|
||||
# and confirm lifecycle.ready() fires, screens navigate, NFC scan starts
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Person 2: Native Shells (KMP SDK + Swift Providers) — Workstream Overview
|
||||
|
||||
> Last updated: 2026-02-19
|
||||
> Last updated: 2026-02-23
|
||||
> Owner: Person 2 (Native Shells)
|
||||
> Project: [../SDK-OVERVIEW.md](../SDK-OVERVIEW.md)
|
||||
> Implementation: [SPEC.md](./SPEC.md)
|
||||
@@ -21,8 +21,9 @@
|
||||
- [x] Delete 4 unnecessary Android handlers (documents, crypto, analytics, haptic — 511 LOC)
|
||||
- [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
|
||||
- [ ] Platform contract alignment is still open (iOS expanded domains vs Android web fallbacks)
|
||||
- [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)
|
||||
|
||||
## What You Own
|
||||
@@ -75,7 +76,7 @@ You build the native shells that sit between the host app and the bridge protoco
|
||||
│ webview-app Vite bundle │
|
||||
└─────────────────────────────────┘
|
||||
|
||||
* iOS implementation now registers 9 handlers (NFC, Camera, Biometrics, SecureStorage, Lifecycle, Documents, Crypto, Analytics, Haptic); Android remains focused on 5 core native handlers. This asymmetry is implemented and needs explicit policy documentation.
|
||||
* iOS implementation now registers 9 handlers (NFC, Camera, Biometrics, SecureStorage, Lifecycle, Documents, Crypto, Analytics, Haptic); Android remains focused on 5 core native handlers. This asymmetry is now explicitly accepted as a compatibility superset contract: Android is the normative minimum, iOS extra handlers must remain behavior-compatible and non-authoritative for web-fallback domains.
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Person 3: Integration Samples — Workstream Overview
|
||||
|
||||
> Last updated: 2026-02-19
|
||||
> Last updated: 2026-02-23
|
||||
> Owner: Person 3 (Integrations)
|
||||
> Project: [../SDK-OVERVIEW.md](../SDK-OVERVIEW.md)
|
||||
> Implementation: [SPEC-MINIPAY-SAMPLE.md](./SPEC-MINIPAY-SAMPLE.md)
|
||||
@@ -17,9 +17,14 @@
|
||||
- [x] MiniPay sample project scaffolded (`packages/kmp-minipay-sample/`)
|
||||
- [x] Android: home screen + SDK launch + result screen wiring present
|
||||
- [x] iOS: Compose Multiplatform launch path is present
|
||||
- [x] Integration hardening paths are implemented in sample result UX (error-code to user-message mapping)
|
||||
- [x] Non-device validation evidence captured:
|
||||
- `@selfxyz/rn-sdk` tests: all passing at merge time (includes NFC failure modes and APDU path handling; see CI checks)
|
||||
- `@selfxyz/webview-bridge` tests: all passing at merge time (see CI checks)
|
||||
- iOS launch E2E artifact: `app/maestro-results.xml` (1 test, 0 failures, 19s on iPhone 16 simulator)
|
||||
- [ ] End-to-end: NFC scan on physical device through sample app still requires validation
|
||||
|
||||
Overall: **Partial** — sample implementation exists and launch wiring is in place; device-level verification validation remains.
|
||||
Overall: **Partial** — implementation and non-device validation are in place; final physical-device NFC verification outcomes remain the blocking gap.
|
||||
|
||||
## What You Own
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Person 4: SDK Core Adaptation — Workstream Overview
|
||||
|
||||
> Last updated: 2026-02-19
|
||||
> Last updated: 2026-02-23
|
||||
> Owner: Person 4 (SDK Core)
|
||||
> Project: [../SDK-OVERVIEW.md](../SDK-OVERVIEW.md)
|
||||
> Implementation: [SPEC.md](./SPEC.md)
|
||||
@@ -21,8 +21,10 @@
|
||||
- [x] WebView Lifecycle Events (Chunk 4C — Done)
|
||||
- [x] Conditional SelfApp Store (Chunk 4E — Done)
|
||||
- [x] Web Fallback Adapter Implementations (Chunk 4F — Done)
|
||||
- [ ] Duplicate fallback ownership remains unresolved (`mobile-sdk-alpha` vs `webview-bridge`)
|
||||
- [ ] Final contract for web-only fallback vs bridge routing (crypto/haptic behavior) still needs explicit decision
|
||||
- [x] Fallback ownership decision documented (`mobile-sdk-alpha` owns WebView-only fallback logic)
|
||||
- [x] Web-only fallback vs bridge-routing contract documented (hybrid crypto + no-op haptic)
|
||||
- [ ] Implementation consolidation still pending (bridge-layer fallback duplicates not yet fully removed)
|
||||
- [ ] `generateKey()`/`getPublicKey()` not exposed in `BridgeCryptoAdapter` interface — methods exist in iOS native handler and bridge protocol types but unreachable from WebView client code
|
||||
|
||||
## What You Own
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Person 5: RN Native Shell — Workstream Overview
|
||||
|
||||
> Last updated: 2026-02-19
|
||||
> Last updated: 2026-02-23
|
||||
> Owner: Person 5 (RN SDK)
|
||||
> Project: [../SDK-OVERVIEW.md](../SDK-OVERVIEW.md)
|
||||
> Implementation: [SPEC.md](./SPEC.md)
|
||||
@@ -19,10 +19,12 @@
|
||||
- [x] `MessageRouter` dispatching bridge messages to handlers is implemented
|
||||
- [x] 5 native handler bridges are implemented (NFC, Camera, Biometrics, Keychain, Lifecycle)
|
||||
- [x] Asset loading strategy is implemented for iOS + Android, including dev override
|
||||
- [x] NFC handler supports APDU command exchange (`apduCommands` -> `apduResponses`)
|
||||
- [x] Camera MRZ bridge is implemented against native scanner modules
|
||||
- [ ] Integration validation in Self Wallet app is still a follow-up validation task
|
||||
- [ ] npm publish (`@selfxyz/rn-sdk`) not completed in this branch
|
||||
|
||||
**Overall: Partial** — package implementation is present; highest-risk carry-forward items are NFC APDU-path deviation and camera/MRZ stub.
|
||||
**Overall: Partial** — package implementation is present; highest-risk carry-forward item is breadth of physical-device integration validation across host apps.
|
||||
|
||||
## What You Own
|
||||
|
||||
|
||||
Reference in New Issue
Block a user