SELF-2336 Export proving machine for webview (#1864)

* Extract Reusable App Adapter Factories for SelfClient Assembly

* update coderabbit comments

* lint
This commit is contained in:
Seshanth.S
2026-03-25 12:02:25 +05:30
committed by GitHub
parent e885351f61
commit 167da8c87f
10 changed files with 201 additions and 56 deletions

View File

@@ -12,8 +12,10 @@ export type {
ClockAdapter,
Config,
CryptoAdapter,
DocumentCatalog,
DocumentsAdapter,
HttpAdapter,
IDDocument,
LogLevel,
LoggerAdapter,
MRZInfo,
@@ -37,10 +39,9 @@ export type {
export type { BaseContext, NFCScanContext, ProofContext } from './proving/internal/logging';
export type { DG1, DG2, ParsedNFCResponse } from './nfc';
export type { PassportValidationCallbacks } from './validation/document';
export type { ProvingStateType, provingMachineCircuitType } from './proving/provingMachine';
export type { ProvingState, ProvingStateType, provingMachineCircuitType } from './proving/provingMachine';
export type { SDKEvent, SDKEventMap } from './types/events';
export type { SdkErrorCategory } from './errors';
export type { WebAnalyticsOptions } from './adapters/browser';
export {
@@ -88,6 +89,8 @@ export { extractMRZInfo, extractNameFromMRZ, formatDateToYYMMDD } from './mrz';
export { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from './mock/generator';
export { getPostVerificationRoute, useProvingStore } from './proving/provingMachine';
export { isPassportDataValid } from './validation/document';
export { mergeConfig } from './config/merge';

View File

@@ -15,11 +15,16 @@
"@selfxyz/mobile-sdk-alpha": "workspace:^",
"@selfxyz/webview-bridge": "workspace:^",
"@sumsub/websdk": "^2.0.0",
"buffer": "^6.0.3",
"elliptic": "^6.5.4",
"lottie-react": "^2.4.0",
"node-forge": "^1.3.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.28.0",
"socket.io-client": "^4.8.3",
"uuid": "^11.1.0",
"xstate": "^5.20.2",
"zustand": "^4.5.2"
},
"devDependencies": {

View File

@@ -2,6 +2,9 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { Buffer } from 'buffer';
globalThis.Buffer = Buffer;
import React from 'react';
import ReactDOM from 'react-dom/client';
import { App } from './App';

View File

@@ -2,48 +2,40 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import type { SelfClient } from '@selfxyz/mobile-sdk-alpha/browser';
import { createSelfClient, createListenersMap } from '@selfxyz/mobile-sdk-alpha/browser';
import {
bridgeCryptoAdapter,
bridgeAuthAdapter,
indexedDBDocumentsAdapter,
bridgeStorageAdapter,
consoleAnalyticsAdapter,
createSdkAdapters,
createKeychainDocumentsAdapter,
bridgeLifecycleAdapter,
webNavigationAdapter,
noOpHapticAdapter,
bridgeHapticAdapter,
bridgeBiometricsAdapter,
consoleAnalyticsAdapter,
} from '@selfxyz/webview-bridge/adapters';
import type {
BridgeCryptoAdapter,
BridgeAuthAdapter,
BridgeDocumentsAdapter,
BridgeStorageAdapter,
BridgeAnalyticsAdapter,
BridgeLifecycleAdapter,
BridgeNavigationAdapter,
BridgeHapticAdapter,
BridgeBiometricsAdapter,
BridgeAnalyticsAdapter,
} from '@selfxyz/webview-bridge/adapters';
import type { DocumentsAdapter } from '@selfxyz/mobile-sdk-alpha/browser';
import { useBridge } from './BridgeProvider';
import { useVerificationRequest } from './VerificationRequestProvider';
export interface SelfClientAdapters {
crypto: BridgeCryptoAdapter;
auth: BridgeAuthAdapter;
documents: BridgeDocumentsAdapter;
storage: BridgeStorageAdapter;
analytics: BridgeAnalyticsAdapter;
export interface WebViewAdapters {
client: SelfClient;
lifecycle: BridgeLifecycleAdapter;
navigation: BridgeNavigationAdapter;
haptic: BridgeHapticAdapter;
biometrics: BridgeBiometricsAdapter;
analytics: BridgeAnalyticsAdapter;
documents: DocumentsAdapter;
}
const SelfClientContext = createContext<SelfClientAdapters | null>(null);
const SelfClientContext = createContext<WebViewAdapters | null>(null);
export function useSelfClient(): SelfClientAdapters {
export function useSelfClient(): WebViewAdapters {
const adapters = useContext(SelfClientContext);
if (!adapters) {
throw new Error('useSelfClient must be used within a SelfClientProvider');
@@ -58,24 +50,40 @@ export const SelfClientProvider: React.FC<{ children: React.ReactNode }> = ({
const navigate = useNavigate();
const { verificationId } = useVerificationRequest();
const adapters = useMemo<SelfClientAdapters>(() => {
const lifecycle = bridgeLifecycleAdapter(bridge);
const navigateRef = useRef(navigate);
useEffect(() => { navigateRef.current = navigate; }, [navigate]);
const stableNavigate = useCallback((path: string) => navigateRef.current(path), []);
const stableGoBack = useCallback(() => navigateRef.current(-1), []);
const webViewAdapters = useMemo<WebViewAdapters>(() => {
const sdkAdapters = createSdkAdapters({
bridge,
navigate: stableNavigate,
goBack: stableGoBack,
});
const { map: listeners } = createListenersMap();
const client = createSelfClient({
config: {
platform: 'webview',
debug: import.meta.env.DEV,
},
adapters: sdkAdapters,
listeners,
});
const documents = createKeychainDocumentsAdapter(bridge);
return {
crypto: bridgeCryptoAdapter(bridge),
auth: bridgeAuthAdapter(bridge),
documents: indexedDBDocumentsAdapter(),
storage: bridgeStorageAdapter(bridge),
analytics: consoleAnalyticsAdapter(),
lifecycle,
navigation: webNavigationAdapter(
(path: string) => navigate(path),
() => navigate(-1),
),
haptic: noOpHapticAdapter(),
client,
lifecycle: bridgeLifecycleAdapter(bridge),
haptic: bridgeHapticAdapter(bridge),
biometrics: bridgeBiometricsAdapter(bridge),
analytics: consoleAnalyticsAdapter(),
documents,
};
}, [bridge, navigate]);
}, [bridge, stableNavigate, stableGoBack]);
const lastReadyRef = useRef<{
lifecycle: BridgeLifecycleAdapter;
@@ -83,16 +91,16 @@ export const SelfClientProvider: React.FC<{ children: React.ReactNode }> = ({
} | null>(null);
useEffect(() => {
if (
lastReadyRef.current?.lifecycle === adapters.lifecycle &&
lastReadyRef.current?.lifecycle === webViewAdapters.lifecycle &&
lastReadyRef.current?.verificationId === verificationId
) {
return;
}
adapters.lifecycle.ready(
webViewAdapters.lifecycle.ready(
verificationId ? { verificationId } : {},
);
lastReadyRef.current = { lifecycle: adapters.lifecycle, verificationId };
}, [adapters.lifecycle, verificationId]);
lastReadyRef.current = { lifecycle: webViewAdapters.lifecycle, verificationId };
}, [webViewAdapters.lifecycle, verificationId]);
useEffect(() => {
return bridge.on('lifecycle', 'cancel', () => {
@@ -101,7 +109,7 @@ export const SelfClientProvider: React.FC<{ children: React.ReactNode }> = ({
}, [bridge, navigate]);
return (
<SelfClientContext.Provider value={adapters}>
<SelfClientContext.Provider value={webViewAdapters}>
{children}
</SelfClientContext.Provider>
);

View File

@@ -47,6 +47,10 @@ export type { BridgeBiometricsAdapter } from './biometrics';
export { bridgeCameraAdapter } from './camera';
export type { BridgeCameraAdapter, MrzScanParams, MrzScanResult } from './camera';
export { createKeychainDocumentsAdapter } from './keychain-documents';
export { createSdkAdapters } from './sdk-adapter-map';
export type { CreateSdkAdaptersOpts } from './sdk-adapter-map';
export function indexedDBDocumentsAdapter(): BridgeDocumentsAdapter {
return createIndexedDBDocumentsAdapter() as BridgeDocumentsAdapter;
}

View File

@@ -0,0 +1,55 @@
// 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.
import type { WebViewBridge } from '../bridge';
import type { DocumentsAdapter, DocumentCatalog, IDDocument } from '@selfxyz/mobile-sdk-alpha/browser';
const CATALOG_KEY = 'self_document_catalog';
const DOC_PREFIX = 'self_doc_';
const EMPTY_CATALOG: DocumentCatalog = { documents: [] };
function safeParse<T>(raw: string, fallback: T): T {
try {
return JSON.parse(raw);
} catch {
return fallback;
}
}
export function createKeychainDocumentsAdapter(bridge: WebViewBridge): DocumentsAdapter {
async function storageGet(key: string): Promise<string | null> {
const result = await bridge.request<{ value: string | null }>(
'secureStorage', 'get', { key },
);
return result?.value ?? null;
}
async function storageSet(key: string, value: string): Promise<void> {
await bridge.request('secureStorage', 'set', { key, value });
}
async function storageRemove(key: string): Promise<void> {
await bridge.request('secureStorage', 'remove', { key });
}
return {
async loadDocumentCatalog(): Promise<DocumentCatalog> {
const raw = await storageGet(CATALOG_KEY);
return raw ? safeParse(raw, EMPTY_CATALOG) : EMPTY_CATALOG;
},
async saveDocumentCatalog(catalog: DocumentCatalog): Promise<void> {
await storageSet(CATALOG_KEY, JSON.stringify(catalog));
},
async loadDocumentById(id: string): Promise<IDDocument | null> {
const raw = await storageGet(`${DOC_PREFIX}${id}`);
return raw ? safeParse<IDDocument | null>(raw, null) : null;
},
async saveDocument(id: string, doc: IDDocument): Promise<void> {
await storageSet(`${DOC_PREFIX}${id}`, JSON.stringify(doc));
},
async deleteDocument(id: string): Promise<void> {
await storageRemove(`${DOC_PREFIX}${id}`);
},
};
}

View File

@@ -0,0 +1,61 @@
// 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.
import type { WebViewBridge } from '../bridge';
import type {
Adapters,
CryptoAdapter,
AuthAdapter,
NavigationAdapter,
RouteName,
} from '@selfxyz/mobile-sdk-alpha/browser';
import { bridgeCryptoAdapter } from './crypto';
import { bridgeAuthAdapter } from './auth';
import { createKeychainDocumentsAdapter } from './keychain-documents';
import {
createWebAnalyticsAdapter,
createWebNetworkAdapter,
webNFCScannerShim,
} from '@selfxyz/mobile-sdk-alpha/browser';
export interface CreateSdkAdaptersOpts {
bridge: WebViewBridge;
navigate: (path: string) => void;
goBack: () => void;
}
export function createSdkAdapters(opts: CreateSdkAdaptersOpts): Adapters {
const { bridge, navigate, goBack } = opts;
const bridgeCrypto = bridgeCryptoAdapter(bridge);
const crypto: CryptoAdapter = {
hash: bridgeCrypto.hash,
sign: bridgeCrypto.sign,
generateKey: bridgeCrypto.generateKey,
getPublicKey: bridgeCrypto.getPublicKey,
};
const bridgeAuth = bridgeAuthAdapter(bridge);
const auth: AuthAdapter = {
getPrivateKey: bridgeAuth.getPrivateKey,
};
const navigation: NavigationAdapter = {
goBack,
goTo: (routeName: RouteName, params?: Record<string, unknown>) => {
const query = params ? `?${new URLSearchParams(params as Record<string, string>)}` : '';
navigate(`/${routeName}${query}`);
},
};
return {
scanner: webNFCScannerShim,
crypto,
network: createWebNetworkAdapter(),
auth,
documents: createKeychainDocumentsAdapter(bridge),
navigation,
analytics: createWebAnalyticsAdapter(),
};
}

View File

@@ -52,7 +52,7 @@ On **March 11, 2026**, the active SDK scope changed to **WebView only, with no c
| WV-04 | Define the host callback contract for launch, dismiss, and final result without native modules | Done | Medium | WV-02 | [plans/WV-04-host-callback-contract.md](./plans/WV-04-host-callback-contract.md) | Browser host fallback now uses `postMessage` for iframe/popup embedding while native transports keep their current behavior |
| WV-05 | Integrate KYC provider Web SDK into ProviderLaunchScreen (Sumsub as default) | In Progress | High | WV-02 | [plans/WV-05-sumsub-web-sdk.md](./plans/WV-05-sumsub-web-sdk.md) | Code complete on `feat/webview-sdk`, needs testing |
| WV-06 | Wire KYC result through verification pipeline to host lifecycle callback | Ready | High | WV-05 | [plans/WV-06-kyc-result-flow.md](./plans/WV-06-kyc-result-flow.md) | Sumsub result → kycResultStore → ConfirmIdentificationScreen → lifecycle.setResult() |
| WV-07 | SelfClient assembly and proving machine export for WebView | Ready | High | SC-03 | [plans/WV-07-selfclient-proving-assembly.md](./plans/WV-07-selfclient-proving-assembly.md) | Export useProvingStore, map bridge→SDK adapters, keychain-backed documents, create real SelfClient |
| WV-07 | SelfClient assembly and proving machine export for WebView | Done | High | SC-03 | [plans/WV-07-selfclient-proving-assembly.md](./plans/WV-07-selfclient-proving-assembly.md) | Export useProvingStore, map bridge→SDK adapters, keychain-backed documents, create real SelfClient |
| WV-08 | Wire tunnel flow with real proving machine (register → disclose) | Ready | High | WV-07 | [plans/WV-08-tunnel-proving-flow.md](./plans/WV-08-tunnel-proving-flow.md) | Replace mock tunnel proving with real provingMachine: Sumsub → store doc → prove → disclose → result |
Allowed statuses: `Ready`, `In Progress`, `Blocked`, `Deferred`, `Done`
@@ -67,7 +67,7 @@ Allowed statuses: `Ready`, `In Progress`, `Blocked`, `Deferred`, `Done`
| [plans/WV-04-host-callback-contract.md](./plans/WV-04-host-callback-contract.md) | WV-04 | Done |
| [plans/WV-05-sumsub-web-sdk.md](./plans/WV-05-sumsub-web-sdk.md) | WV-05 | In Progress (code complete, needs testing) |
| [plans/WV-06-kyc-result-flow.md](./plans/WV-06-kyc-result-flow.md) | WV-06 | Ready |
| [plans/WV-07-selfclient-proving-assembly.md](./plans/WV-07-selfclient-proving-assembly.md) | WV-07 | Ready |
| [plans/WV-07-selfclient-proving-assembly.md](./plans/WV-07-selfclient-proving-assembly.md) | WV-07 | Done |
| [plans/WV-08-tunnel-proving-flow.md](./plans/WV-08-tunnel-proving-flow.md) | WV-08 | Ready |
## Completion Checklist

View File

@@ -1,7 +1,7 @@
# WV-07: SelfClient Assembly & Proving Machine Export for WebView
> Last updated: 2026-03-24
> Status: Ready
> Status: Done
> Priority: High
> Depends on: SC-03 (Ready — creates `createWebNetworkAdapter()`)
@@ -327,15 +327,16 @@ cd packages/webview-app && yarn build
## Definition of Done
- [ ] `useProvingStore` and `ProvingState` exported from `browser.ts`
- [ ] `createKeychainDocumentsAdapter()` persists documents via secureStorage bridge
- [ ] `createSdkAdapters()` maps bridge adapters to SDK Adapters interface
- [ ] `SelfClientProvider` creates a real `SelfClient` via `createSelfClient()`
- [ ] Buffer polyfill added to webview-app entry point
- [ ] `yarn types` clean in mobile-sdk-alpha
- [ ] `yarn build` clean in webview-bridge and webview-app
- [ ] Backlog row added in SPEC.md
- [x] `useProvingStore` and `ProvingState` exported from `browser.ts`
- [x] `createKeychainDocumentsAdapter()` persists documents via secureStorage bridge
- [x] `createSdkAdapters()` maps bridge adapters to SDK Adapters interface
- [x] `SelfClientProvider` creates a real `SelfClient` via `createSelfClient()`
- [x] Buffer polyfill added to webview-app entry point
- [x] `yarn types` clean in mobile-sdk-alpha
- [x] `yarn build` clean in webview-bridge and webview-app
- [x] Backlog row added in SPEC.md
## Status Log
- 2026-03-24: Plan created.
- 2026-03-25: Implementation complete. All validation passes. Branch: feat/proving-machine-export-wv-07

View File

@@ -11141,14 +11141,19 @@ __metadata:
"@types/react": "npm:^18.3.4"
"@types/react-dom": "npm:^18.3.0"
"@vitejs/plugin-react": "npm:^4.3.4"
buffer: "npm:^6.0.3"
elliptic: "npm:^6.5.4"
lottie-react: "npm:^2.4.0"
node-forge: "npm:^1.3.3"
react: "npm:^18.3.1"
react-dom: "npm:^18.3.1"
react-router-dom: "npm:^6.28.0"
socket.io-client: "npm:^4.8.3"
typescript: "npm:^5.9.3"
uuid: "npm:^11.1.0"
vite: "npm:^6.1.0"
vitest: "npm:^2.1.8"
xstate: "npm:^5.20.2"
zustand: "npm:^4.5.2"
languageName: unknown
linkType: soft
@@ -22047,7 +22052,7 @@ __metadata:
languageName: node
linkType: hard
"elliptic@npm:6.6.1, elliptic@npm:^6.5.5, elliptic@npm:^6.6.1":
"elliptic@npm:6.6.1, elliptic@npm:^6.5.4, elliptic@npm:^6.5.5, elliptic@npm:^6.6.1":
version: 6.6.1
resolution: "elliptic@npm:6.6.1"
dependencies: