From 167da8c87f40d826b60c296e12918d1e75a46e7d Mon Sep 17 00:00:00 2001 From: "Seshanth.S" <35675963+seshanthS@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:02:25 +0530 Subject: [PATCH] SELF-2336 Export proving machine for webview (#1864) * Extract Reusable App Adapter Factories for SelfClient Assembly * update coderabbit comments * lint --- packages/mobile-sdk-alpha/src/browser.ts | 7 +- packages/webview-app/package.json | 5 + packages/webview-app/src/main.tsx | 3 + .../src/providers/SelfClientProvider.tsx | 92 ++++++++++--------- packages/webview-bridge/src/adapters/index.ts | 4 + .../src/adapters/keychain-documents.ts | 55 +++++++++++ .../src/adapters/sdk-adapter-map.ts | 61 ++++++++++++ .../projects/sdk/workstreams/webview/SPEC.md | 4 +- .../WV-07-selfclient-proving-assembly.md | 19 ++-- yarn.lock | 7 +- 10 files changed, 201 insertions(+), 56 deletions(-) create mode 100644 packages/webview-bridge/src/adapters/keychain-documents.ts create mode 100644 packages/webview-bridge/src/adapters/sdk-adapter-map.ts diff --git a/packages/mobile-sdk-alpha/src/browser.ts b/packages/mobile-sdk-alpha/src/browser.ts index b01dfd536..f60cb3b9b 100644 --- a/packages/mobile-sdk-alpha/src/browser.ts +++ b/packages/mobile-sdk-alpha/src/browser.ts @@ -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'; diff --git a/packages/webview-app/package.json b/packages/webview-app/package.json index 1b744ae7f..a5cec9e47 100644 --- a/packages/webview-app/package.json +++ b/packages/webview-app/package.json @@ -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": { diff --git a/packages/webview-app/src/main.tsx b/packages/webview-app/src/main.tsx index 767b6d2a3..987650f6d 100644 --- a/packages/webview-app/src/main.tsx +++ b/packages/webview-app/src/main.tsx @@ -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'; diff --git a/packages/webview-app/src/providers/SelfClientProvider.tsx b/packages/webview-app/src/providers/SelfClientProvider.tsx index 0081ec957..9b8df70e0 100644 --- a/packages/webview-app/src/providers/SelfClientProvider.tsx +++ b/packages/webview-app/src/providers/SelfClientProvider.tsx @@ -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(null); +const SelfClientContext = createContext(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(() => { - 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(() => { + 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 ( - + {children} ); diff --git a/packages/webview-bridge/src/adapters/index.ts b/packages/webview-bridge/src/adapters/index.ts index 6f714c63b..c92ef8ae8 100644 --- a/packages/webview-bridge/src/adapters/index.ts +++ b/packages/webview-bridge/src/adapters/index.ts @@ -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; } diff --git a/packages/webview-bridge/src/adapters/keychain-documents.ts b/packages/webview-bridge/src/adapters/keychain-documents.ts new file mode 100644 index 000000000..22a90f10f --- /dev/null +++ b/packages/webview-bridge/src/adapters/keychain-documents.ts @@ -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(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 { + const result = await bridge.request<{ value: string | null }>( + 'secureStorage', 'get', { key }, + ); + return result?.value ?? null; + } + + async function storageSet(key: string, value: string): Promise { + await bridge.request('secureStorage', 'set', { key, value }); + } + + async function storageRemove(key: string): Promise { + await bridge.request('secureStorage', 'remove', { key }); + } + + return { + async loadDocumentCatalog(): Promise { + const raw = await storageGet(CATALOG_KEY); + return raw ? safeParse(raw, EMPTY_CATALOG) : EMPTY_CATALOG; + }, + async saveDocumentCatalog(catalog: DocumentCatalog): Promise { + await storageSet(CATALOG_KEY, JSON.stringify(catalog)); + }, + async loadDocumentById(id: string): Promise { + const raw = await storageGet(`${DOC_PREFIX}${id}`); + return raw ? safeParse(raw, null) : null; + }, + async saveDocument(id: string, doc: IDDocument): Promise { + await storageSet(`${DOC_PREFIX}${id}`, JSON.stringify(doc)); + }, + async deleteDocument(id: string): Promise { + await storageRemove(`${DOC_PREFIX}${id}`); + }, + }; +} diff --git a/packages/webview-bridge/src/adapters/sdk-adapter-map.ts b/packages/webview-bridge/src/adapters/sdk-adapter-map.ts new file mode 100644 index 000000000..44271f4fa --- /dev/null +++ b/packages/webview-bridge/src/adapters/sdk-adapter-map.ts @@ -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) => { + const query = params ? `?${new URLSearchParams(params as Record)}` : ''; + navigate(`/${routeName}${query}`); + }, + }; + + return { + scanner: webNFCScannerShim, + crypto, + network: createWebNetworkAdapter(), + auth, + documents: createKeychainDocumentsAdapter(bridge), + navigation, + analytics: createWebAnalyticsAdapter(), + }; +} diff --git a/specs/projects/sdk/workstreams/webview/SPEC.md b/specs/projects/sdk/workstreams/webview/SPEC.md index 4dc6c303c..f9bcec806 100644 --- a/specs/projects/sdk/workstreams/webview/SPEC.md +++ b/specs/projects/sdk/workstreams/webview/SPEC.md @@ -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 diff --git a/specs/projects/sdk/workstreams/webview/plans/WV-07-selfclient-proving-assembly.md b/specs/projects/sdk/workstreams/webview/plans/WV-07-selfclient-proving-assembly.md index 73deb80d8..aa5731318 100644 --- a/specs/projects/sdk/workstreams/webview/plans/WV-07-selfclient-proving-assembly.md +++ b/specs/projects/sdk/workstreams/webview/plans/WV-07-selfclient-proving-assembly.md @@ -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 diff --git a/yarn.lock b/yarn.lock index 36d392ad9..381706b82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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: