mirror of
https://github.com/selfxyz/self.git
synced 2026-04-27 03:01:15 -04:00
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:
@@ -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';
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
55
packages/webview-bridge/src/adapters/keychain-documents.ts
Normal file
55
packages/webview-bridge/src/adapters/keychain-documents.ts
Normal 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}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
61
packages/webview-bridge/src/adapters/sdk-adapter-map.ts
Normal file
61
packages/webview-bridge/src/adapters/sdk-adapter-map.ts
Normal 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(),
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user