mirror of
https://github.com/selfxyz/self.git
synced 2026-02-19 02:24:25 -05:00
621 lines
23 KiB
Markdown
621 lines
23 KiB
Markdown
# Person 1: UI / WebView / Bridge — Implementation Spec
|
|
|
|
## Overview
|
|
|
|
You are building the **web side** of the Self Mobile SDK. This means:
|
|
|
|
1. **`packages/webview-bridge/`** — JS bridge protocol library (npm package `@selfxyz/webview-bridge`)
|
|
2. **`packages/webview-app/`** — Vite-bundled React app that runs inside a native WebView
|
|
|
|
The output of `vite build` (a single `index.html` + JS bundle) gets bundled into the KMP SDK artifact by Person 2. You don't need to worry about native code — just make sure the bridge protocol is correct and the screens work.
|
|
|
|
---
|
|
|
|
## What to Delete First
|
|
|
|
Delete these directories entirely before starting (they're from the previous prototype):
|
|
- `packages/webview-bridge/`
|
|
- `packages/webview-app/`
|
|
|
|
The prototype was useful for learning. The architecture and bridge protocol are sound. You're recreating them from scratch with proper structure.
|
|
|
|
---
|
|
|
|
## Package 1: `@selfxyz/webview-bridge`
|
|
|
|
### Purpose
|
|
|
|
TypeScript library that handles all communication between the WebView and native shell. Provides:
|
|
- `WebViewBridge` class — manages request/response lifecycle, event subscriptions, timeouts
|
|
- Bridge adapter factories — one per `mobile-sdk-alpha` adapter interface
|
|
- `MockNativeBridge` — test utility for unit/integration tests without native
|
|
- Protocol types and JSON schema validation
|
|
|
|
### Package Structure
|
|
|
|
```
|
|
packages/webview-bridge/
|
|
src/
|
|
types.ts # Protocol types (BridgeDomain, BridgeRequest, etc.)
|
|
bridge.ts # WebViewBridge class
|
|
schema.ts # Message validation
|
|
mock.ts # MockNativeBridge for testing
|
|
adapters/
|
|
nfc-scanner.ts # NFCScannerAdapter → nfc.scan bridge
|
|
crypto.ts # CryptoAdapter (hash=WebCrypto, sign=bridge)
|
|
auth.ts # AuthAdapter → secureStorage.get with biometric
|
|
documents.ts # DocumentsAdapter → documents.* bridge
|
|
storage.ts # StorageAdapter → secureStorage.* bridge
|
|
analytics.ts # AnalyticsAdapter → analytics.* bridge (fire-and-forget)
|
|
haptic.ts # HapticAdapter → haptic.trigger bridge
|
|
navigation.ts # NavigationAdapter → React Router (no bridge)
|
|
lifecycle.ts # LifecycleAdapter → lifecycle.* bridge
|
|
index.ts # Re-exports all adapters
|
|
__tests__/
|
|
bridge.test.ts # WebViewBridge unit tests
|
|
schema.test.ts # Validation tests
|
|
adapters.test.ts # Adapter integration tests with MockNativeBridge
|
|
index.ts # Public exports
|
|
package.json
|
|
tsconfig.json
|
|
tsup.config.ts
|
|
```
|
|
|
|
### package.json
|
|
|
|
```json
|
|
{
|
|
"name": "@selfxyz/webview-bridge",
|
|
"version": "0.0.1-alpha.1",
|
|
"type": "module",
|
|
"exports": {
|
|
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" },
|
|
"./mock": { "types": "./dist/mock.d.ts", "import": "./dist/mock.js", "require": "./dist/mock.cjs" },
|
|
"./schema": { "types": "./dist/schema.d.ts", "import": "./dist/schema.js", "require": "./dist/schema.cjs" },
|
|
"./adapters": { "types": "./dist/adapters.d.ts", "import": "./dist/adapters.js", "require": "./dist/adapters.cjs" }
|
|
},
|
|
"main": "./dist/index.cjs",
|
|
"module": "./dist/index.js",
|
|
"types": "./dist/index.d.ts",
|
|
"files": ["dist"],
|
|
"scripts": {
|
|
"build": "tsup",
|
|
"test": "vitest run",
|
|
"typecheck": "tsc --noEmit"
|
|
},
|
|
"dependencies": {
|
|
"uuid": "^11.1.0"
|
|
},
|
|
"devDependencies": {
|
|
"@types/node": "^22.18.3",
|
|
"tsup": "^8.0.1",
|
|
"typescript": "^5.9.3",
|
|
"vitest": "^2.1.8"
|
|
},
|
|
"packageManager": "yarn@4.12.0"
|
|
}
|
|
```
|
|
|
|
### tsup.config.ts
|
|
|
|
```typescript
|
|
import { defineConfig } from 'tsup';
|
|
|
|
export default defineConfig({
|
|
entry: {
|
|
index: 'src/index.ts',
|
|
mock: 'src/mock.ts',
|
|
schema: 'src/schema.ts',
|
|
adapters: 'src/adapters/index.ts',
|
|
},
|
|
format: ['esm', 'cjs'],
|
|
dts: true,
|
|
clean: true,
|
|
splitting: false,
|
|
sourcemap: true,
|
|
});
|
|
```
|
|
|
|
### Key Implementation Details
|
|
|
|
#### types.ts — Protocol Types
|
|
|
|
The existing prototype types are correct. Key types to implement:
|
|
|
|
```typescript
|
|
export const BRIDGE_PROTOCOL_VERSION = 1;
|
|
export const DEFAULT_TIMEOUT_MS = 30_000;
|
|
|
|
export type BridgeDomain =
|
|
| 'nfc' | 'biometrics' | 'secureStorage' | 'camera'
|
|
| 'crypto' | 'haptic' | 'analytics' | 'lifecycle'
|
|
| 'documents' | 'navigation';
|
|
|
|
export type BridgeMessageType = 'request' | 'response' | 'event';
|
|
|
|
export interface BridgeError {
|
|
code: string;
|
|
message: string;
|
|
details?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface BridgeRequest { type: 'request'; version: number; id: string; domain: BridgeDomain; method: string; params: Record<string, unknown>; timestamp: number; }
|
|
export interface BridgeResponse { type: 'response'; version: number; id: string; domain: BridgeDomain; requestId: string; success: boolean; data?: unknown; error?: BridgeError; timestamp: number; }
|
|
export interface BridgeEvent { type: 'event'; version: number; id: string; domain: BridgeDomain; event: string; data: unknown; timestamp: number; }
|
|
|
|
// Domain-specific method types
|
|
export type NfcMethod = 'scan' | 'cancelScan' | 'isSupported';
|
|
export type NfcEvent = 'scanProgress' | 'tagDiscovered' | 'scanError';
|
|
export type BiometricsMethod = 'authenticate' | 'isAvailable' | 'getBiometryType';
|
|
export type SecureStorageMethod = 'get' | 'set' | 'remove';
|
|
export type CameraMethod = 'scanMRZ' | 'isAvailable';
|
|
export type CryptoMethod = 'sign' | 'generateKey' | 'getPublicKey';
|
|
export type HapticMethod = 'trigger';
|
|
export type AnalyticsMethod = 'trackEvent' | 'trackNfcEvent' | 'logNfcEvent';
|
|
export type LifecycleMethod = 'ready' | 'dismiss' | 'setResult';
|
|
export type DocumentsMethod = 'loadCatalog' | 'saveCatalog' | 'loadById' | 'save' | 'delete';
|
|
export type NavigationMethod = 'goBack' | 'goTo';
|
|
|
|
// NFC-specific param/result types
|
|
export interface NfcScanParams {
|
|
passportNumber: string;
|
|
dateOfBirth: string;
|
|
dateOfExpiry: string;
|
|
canNumber?: string;
|
|
skipPACE?: boolean;
|
|
skipCA?: boolean;
|
|
extendedMode?: boolean;
|
|
usePacePolling?: boolean;
|
|
sessionId: string;
|
|
useCan?: boolean;
|
|
userId?: string;
|
|
}
|
|
|
|
export interface NfcScanProgress { step: string; percent: number; message?: string; }
|
|
export interface BiometricAuthParams { reason: string; fallbackLabel?: string; }
|
|
export interface VerificationResult { success: boolean; userId?: string; verificationId?: string; proof?: unknown; claims?: Record<string, unknown>; error?: BridgeError; }
|
|
```
|
|
|
|
#### bridge.ts — WebViewBridge Class
|
|
|
|
The existing prototype is solid. Key behaviors:
|
|
|
|
1. **Constructor**: Auto-detects native transport (Android `SelfNativeAndroid`, iOS `webkit.messageHandlers.SelfNativeIOS`), registers `window.SelfNativeBridge` global
|
|
2. **`request(domain, method, params, timeout?)`**: Creates request with UUID, sets up pending promise with timeout, sends via transport
|
|
3. **`fire(domain, method, params)`**: Same as request but no pending promise (fire-and-forget)
|
|
4. **`on(domain, event, handler)`**: Subscribe to native events, returns unsubscribe function
|
|
5. **`handleMessage(json)`**: Called by native via `_handleResponse`/`_handleEvent`, dispatches to pending or listeners
|
|
6. **`destroy()`**: Rejects all pending, clears listeners, removes global
|
|
|
|
**Transport detection:**
|
|
```typescript
|
|
// Android
|
|
if (globalThis.SelfNativeAndroid?.postMessage) { ... }
|
|
// iOS
|
|
if (globalThis.webkit?.messageHandlers?.SelfNativeIOS?.postMessage) { ... }
|
|
```
|
|
|
|
**Important:** The iOS handler name changed from the prototype. Person 2's spec says `SelfNativeIOS` as the WKScriptMessageHandler name. Make sure this matches.
|
|
|
|
#### Adapter Factories
|
|
|
|
Each adapter factory takes a `WebViewBridge` instance and returns an object conforming to the corresponding `mobile-sdk-alpha` adapter interface.
|
|
|
|
**NFC Scanner** (`nfc-scanner.ts`):
|
|
- `scan(opts)`: Calls `bridge.request('nfc', 'scan', params, 120_000)` with 120s timeout
|
|
- Handles `AbortSignal` — if aborted, fires `nfc.cancelScan` and rejects
|
|
- Helper `onNfcProgress(bridge, handler)` subscribes to `nfc:scanProgress` events
|
|
|
|
**Crypto** (`crypto.ts`):
|
|
- `hash(input, algo)`: Uses Web Crypto API (`crypto.subtle.digest`), no bridge round-trip
|
|
- `sign(data, keyRef)`: Encodes data as base64, calls `bridge.request('crypto', 'sign', { data, keyRef })`, decodes base64 result
|
|
|
|
**Auth** (`auth.ts`):
|
|
- `getPrivateKey()`: Calls `bridge.request('secureStorage', 'get', { key: 'self_private_key', requireBiometric: true })`, returns `null` on error
|
|
|
|
**Documents** (`documents.ts`):
|
|
- `loadDocumentCatalog()`: `bridge.request('documents', 'loadCatalog')`
|
|
- `saveDocumentCatalog(catalog)`: `bridge.request('documents', 'saveCatalog', { catalog })`
|
|
- `loadDocumentById(id)`: `bridge.request('documents', 'loadById', { id })`
|
|
- `saveDocument(id, data)`: `bridge.request('documents', 'save', { id, data })`
|
|
- `deleteDocument(id)`: `bridge.request('documents', 'delete', { id })`
|
|
|
|
**Storage** (`storage.ts`):
|
|
- `get(key)`: `bridge.request('secureStorage', 'get', { key })`
|
|
- `set(key, value)`: `bridge.request('secureStorage', 'set', { key, value })`
|
|
- `remove(key)`: `bridge.request('secureStorage', 'remove', { key })`
|
|
|
|
**Analytics** (`analytics.ts`) — all fire-and-forget:
|
|
- `trackEvent(event, payload)`: `bridge.fire('analytics', 'trackEvent', { event, payload })`
|
|
- `trackNfcEvent(name, properties)`: `bridge.fire('analytics', 'trackNfcEvent', { name, properties })`
|
|
- `logNFCEvent(level, message, context, details)`: `bridge.fire('analytics', 'logNfcEvent', { level, message, context, details })`
|
|
|
|
**Haptic** (`haptic.ts`):
|
|
- `trigger(type)`: `bridge.fire('haptic', 'trigger', { type })`
|
|
|
|
**Navigation** (`navigation.ts`) — NO bridge round-trip, uses React Router:
|
|
- `goBack()`: Calls provided `goBack` callback
|
|
- `goTo(routeName, params)`: Maps `RouteName` to URL path, calls provided `navigate` callback
|
|
|
|
Route map:
|
|
```typescript
|
|
const routeMap: Record<RouteName, string> = {
|
|
DocumentCamera: '/onboarding/camera',
|
|
DocumentOnboarding: '/onboarding',
|
|
CountryPicker: '/onboarding/country',
|
|
IDPicker: '/onboarding/id-type',
|
|
DocumentNFCScan: '/onboarding/nfc',
|
|
ManageDocuments: '/documents',
|
|
Home: '/',
|
|
AccountVerifiedSuccess: '/account/verified',
|
|
AccountRecoveryChoice: '/account/recovery',
|
|
SaveRecoveryPhrase: '/account/recovery/phrase',
|
|
ComingSoon: '/coming-soon',
|
|
DocumentDataNotFound: '/error/no-data',
|
|
Settings: '/settings',
|
|
};
|
|
```
|
|
|
|
**Lifecycle** (`lifecycle.ts`):
|
|
- `ready()`: `bridge.fire('lifecycle', 'ready', {})`
|
|
- `dismiss()`: `bridge.fire('lifecycle', 'dismiss', {})`
|
|
- `setResult(result)`: `bridge.request('lifecycle', 'setResult', result)` — this one awaits
|
|
|
|
#### MockNativeBridge
|
|
|
|
Test utility that implements `NativeTransport`. Intercepts outgoing messages, routes to registered mock handlers, and sends responses back:
|
|
|
|
- `handle(domain, method, handler)`: Register a mock handler
|
|
- `handleWith(domain, method, data)`: Register a handler that returns a fixed value
|
|
- `handleWithError(domain, method, error)`: Register a handler that throws
|
|
- `pushEvent(domain, event, data)`: Simulate a native event
|
|
- `messages`: Get all sent messages for assertions
|
|
- `messagesFor(domain)`: Filter by domain
|
|
|
|
### Validation & Testing
|
|
|
|
```bash
|
|
cd packages/webview-bridge
|
|
npm run build # tsup → dist/
|
|
npx vitest run # unit tests
|
|
npx tsc --noEmit # type-check
|
|
```
|
|
|
|
---
|
|
|
|
## Package 2: `@selfxyz/webview-app`
|
|
|
|
### Purpose
|
|
|
|
A private Vite-bundled React app that runs inside the native WebView. It:
|
|
1. Renders all screens using Tamagui
|
|
2. Wires screens to `mobile-sdk-alpha` via Zustand stores
|
|
3. Uses `@selfxyz/webview-bridge` adapters to connect SDK operations to native
|
|
|
|
### Package Structure
|
|
|
|
```
|
|
packages/webview-app/
|
|
public/
|
|
fonts/
|
|
Advercase-Regular.otf # Copy from app/web/fonts/
|
|
DINOT-Medium.otf
|
|
DINOT-Bold.otf
|
|
IBMPlexMono-Regular.otf
|
|
src/
|
|
main.tsx # Entry point: TamaguiProvider, BridgeProvider, SelfClientProvider, Router
|
|
App.tsx # React Router routes
|
|
fonts.css # @font-face declarations
|
|
reset.css # CSS reset
|
|
providers/
|
|
BridgeProvider.tsx # Creates and provides WebViewBridge instance
|
|
SelfClientProvider.tsx # Creates adapters, wires to mobile-sdk-alpha, signals lifecycle.ready()
|
|
screens/
|
|
onboarding/
|
|
CountryPickerScreen.tsx
|
|
IDSelectionScreen.tsx
|
|
DocumentCameraScreen.tsx
|
|
DocumentNFCScreen.tsx
|
|
ConfirmIdentificationScreen.tsx
|
|
proving/
|
|
ProvingScreen.tsx
|
|
VerificationResultScreen.tsx
|
|
home/
|
|
HomeScreen.tsx
|
|
account/
|
|
SettingsScreen.tsx
|
|
ComingSoonScreen.tsx
|
|
tamagui.config.ts # Shared Tamagui config (custom fonts)
|
|
vite.config.ts
|
|
tsconfig.json
|
|
package.json
|
|
index.html
|
|
```
|
|
|
|
### package.json
|
|
|
|
```json
|
|
{
|
|
"name": "@selfxyz/webview-app",
|
|
"version": "0.0.1-alpha.1",
|
|
"private": true,
|
|
"type": "module",
|
|
"scripts": {
|
|
"dev": "vite",
|
|
"build": "tsc --noEmit && vite build",
|
|
"preview": "vite preview",
|
|
"typecheck": "tsc --noEmit"
|
|
},
|
|
"dependencies": {
|
|
"@selfxyz/mobile-sdk-alpha": "workspace:^",
|
|
"@selfxyz/webview-bridge": "workspace:^",
|
|
"@tamagui/config": "1.126.14",
|
|
"lottie-react": "^2.4.0",
|
|
"react": "^18.3.1",
|
|
"react-dom": "^18.3.1",
|
|
"react-native-web": "^0.19.13",
|
|
"react-router-dom": "^6.28.0",
|
|
"tamagui": "1.126.14",
|
|
"zustand": "^4.5.2"
|
|
},
|
|
"devDependencies": {
|
|
"@tamagui/vite-plugin": "1.126.14",
|
|
"@testing-library/react": "^14.1.2",
|
|
"@types/react": "^18.3.4",
|
|
"@types/react-dom": "^18.3.0",
|
|
"@vitejs/plugin-react": "^4.3.4",
|
|
"typescript": "^5.9.3",
|
|
"vite": "^6.1.0",
|
|
"vitest": "^2.1.8"
|
|
},
|
|
"packageManager": "yarn@4.12.0"
|
|
}
|
|
```
|
|
|
|
### vite.config.ts
|
|
|
|
```typescript
|
|
import { resolve } from 'path';
|
|
import { defineConfig } from 'vite';
|
|
import react from '@vitejs/plugin-react';
|
|
import { tamaguiPlugin } from '@tamagui/vite-plugin';
|
|
|
|
export default defineConfig({
|
|
resolve: {
|
|
extensions: ['.web.tsx', '.web.ts', '.web.js', '.tsx', '.ts', '.js'],
|
|
alias: {
|
|
'react-native': 'react-native-web',
|
|
'lottie-react-native': 'lottie-react',
|
|
},
|
|
},
|
|
plugins: [
|
|
react(),
|
|
tamaguiPlugin({
|
|
config: resolve(__dirname, 'tamagui.config.ts'),
|
|
components: ['tamagui'],
|
|
enableDynamicEvaluation: true,
|
|
excludeReactNativeWebExports: ['Switch', 'ProgressBar', 'Picker', 'CheckBox', 'Touchable'],
|
|
platform: 'web',
|
|
optimize: true,
|
|
}),
|
|
],
|
|
define: { global: 'globalThis' },
|
|
build: {
|
|
target: ['chrome90', 'safari15'],
|
|
rollupOptions: { output: { manualChunks: undefined } },
|
|
assetsInlineLimit: 102400, // Inline assets <100KB (fonts, small images)
|
|
outDir: 'dist',
|
|
emptyOutDir: true,
|
|
sourcemap: true,
|
|
},
|
|
server: { host: '0.0.0.0', port: 5173 },
|
|
});
|
|
```
|
|
|
|
### Key Files
|
|
|
|
#### main.tsx
|
|
|
|
```tsx
|
|
import React from 'react';
|
|
import ReactDOM from 'react-dom/client';
|
|
import { TamaguiProvider, View } from 'tamagui';
|
|
import tamaguiConfig from '../tamagui.config';
|
|
import { App } from './App';
|
|
import { BridgeProvider } from './providers/BridgeProvider';
|
|
import { SelfClientProvider } from './providers/SelfClientProvider';
|
|
import './fonts.css';
|
|
import './reset.css';
|
|
|
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
<React.StrictMode>
|
|
<TamaguiProvider config={tamaguiConfig}>
|
|
<View flex={1} height="100vh" width="100%">
|
|
<BridgeProvider>
|
|
<SelfClientProvider>
|
|
<App />
|
|
</SelfClientProvider>
|
|
</BridgeProvider>
|
|
</View>
|
|
</TamaguiProvider>
|
|
</React.StrictMode>,
|
|
);
|
|
```
|
|
|
|
#### App.tsx — Routes
|
|
|
|
```tsx
|
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
|
// Import all screen components...
|
|
|
|
export const App: React.FC = () => (
|
|
<BrowserRouter>
|
|
<Routes>
|
|
<Route path="/" element={<HomeScreen />} />
|
|
<Route path="/onboarding/country" element={<CountryPickerScreen />} />
|
|
<Route path="/onboarding/id-type" element={<IDSelectionScreen />} />
|
|
<Route path="/onboarding/camera" element={<DocumentCameraScreen />} />
|
|
<Route path="/onboarding/nfc" element={<DocumentNFCScreen />} />
|
|
<Route path="/onboarding/confirm" element={<ConfirmIdentificationScreen />} />
|
|
<Route path="/proving" element={<ProvingScreen />} />
|
|
<Route path="/proving/result" element={<VerificationResultScreen />} />
|
|
<Route path="/settings" element={<SettingsScreen />} />
|
|
<Route path="/account/verified" element={<VerificationResultScreen />} />
|
|
<Route path="/coming-soon" element={<ComingSoonScreen />} />
|
|
<Route path="*" element={<Navigate to="/" replace />} />
|
|
</Routes>
|
|
</BrowserRouter>
|
|
);
|
|
```
|
|
|
|
#### BridgeProvider.tsx
|
|
|
|
Creates a singleton `WebViewBridge` instance with debug logging in dev mode. Provides it via React context.
|
|
|
|
```tsx
|
|
const bridge = useMemo(() => new WebViewBridge({ debug: import.meta.env.DEV }), []);
|
|
```
|
|
|
|
#### SelfClientProvider.tsx
|
|
|
|
Creates all bridge adapters, wires navigation to React Router, and signals `lifecycle.ready()` on mount.
|
|
|
|
```tsx
|
|
// Creates adapters:
|
|
const adapters = {
|
|
scanner: bridgeNFCScannerAdapter(bridge),
|
|
crypto: bridgeCryptoAdapter(bridge),
|
|
auth: bridgeAuthAdapter(bridge),
|
|
documents: bridgeDocumentsAdapter(bridge),
|
|
storage: bridgeStorageAdapter(bridge),
|
|
analytics: bridgeAnalyticsAdapter(bridge),
|
|
navigation: webNavigationAdapter(navigate, goBack),
|
|
};
|
|
const lifecycle = bridgeLifecycleAdapter(bridge);
|
|
|
|
// Signals ready on mount:
|
|
useEffect(() => { lifecycle.ready(); }, []);
|
|
```
|
|
|
|
### Screen Design Pattern
|
|
|
|
Every screen uses Tamagui components, imports colors/fonts from `@selfxyz/mobile-sdk-alpha/constants`, and accesses SDK via `useSelfClient()` hook.
|
|
|
|
```tsx
|
|
import { Text, View, YStack, XStack, ScrollView, Button, Spinner } from 'tamagui';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { black, white, slate300, slate500, amber50 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
|
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
|
import { useSelfClient } from '../../providers/SelfClientProvider';
|
|
```
|
|
|
|
**Consistent patterns across screens:**
|
|
- Header with back button (left arrow `\u2190`) and title
|
|
- `YStack flex={1} backgroundColor={white}` as page wrapper
|
|
- `fontFamily={dinot}` for all text
|
|
- `pressStyle={{ opacity: 0.7 }}` for tap feedback
|
|
- Bottom fixed action buttons
|
|
- `Spinner` from tamagui for loading states
|
|
|
|
### Tamagui Config
|
|
|
|
Same as `app/tamagui.config.ts`:
|
|
|
|
```typescript
|
|
import { createFont, createTamagui } from 'tamagui';
|
|
import { config } from '@tamagui/config/v3';
|
|
|
|
// Custom sizes, lineHeights, letterSpacing scales
|
|
// Custom fonts: advercase, dinot, plexMono
|
|
const appConfig = createTamagui({
|
|
...config,
|
|
fonts: { ...config.fonts, advercase: advercaseFont, dinot: dinotFont, plexMono: plexMonoFont },
|
|
});
|
|
```
|
|
|
|
### Font Setup
|
|
|
|
Copy `app/web/fonts/*.otf` into `packages/webview-app/public/fonts/`.
|
|
|
|
CSS (`fonts.css`):
|
|
```css
|
|
@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-display: swap; }
|
|
@font-face { font-family: 'DINOT-Medium'; src: url('/fonts/DINOT-Medium.otf') format('opentype'); font-display: swap; }
|
|
@font-face { font-family: 'IBMPlexMono-Regular'; src: url('/fonts/IBMPlexMono-Regular.otf') format('opentype'); font-display: swap; }
|
|
```
|
|
|
|
---
|
|
|
|
## Screen Reference (from existing RN app)
|
|
|
|
Use these existing app screens as UI reference for what the screens should look like and do:
|
|
|
|
| WebView Screen | RN App Reference | Key Elements |
|
|
|---------------|-----------------|--------------|
|
|
| CountryPickerScreen | `app/src/screens/documents/selection/CountryPickerScreen.tsx` | Search input, country list with flags |
|
|
| IDSelectionScreen | `app/src/screens/documents/selection/IDPickerScreen.tsx` | Grid of ID document types |
|
|
| DocumentCameraScreen | `app/src/screens/documents/scanning/DocumentCameraScreen.tsx` | MRZ camera view (calls `camera.scanMRZ`) |
|
|
| DocumentNFCScreen | `app/src/screens/documents/scanning/DocumentNFCScanScreen.tsx` | NFC scan progress, Lottie animation |
|
|
| ConfirmIdentificationScreen | `app/src/screens/documents/selection/ConfirmBelongingScreen.tsx` | Document preview, confirm/retry |
|
|
| ProvingScreen | `app/src/screens/verification/ProveScreen.tsx` | Disclosure items list, verify button |
|
|
| VerificationResultScreen | `app/src/screens/onboarding/AccountVerifiedSuccessScreen.tsx` | Success/failure with Lottie |
|
|
| HomeScreen | `app/src/screens/home/HomeScreen.tsx` | Document cards, points section |
|
|
| SettingsScreen | `app/src/screens/account/settings/SettingsScreen.tsx` | Settings list |
|
|
| ComingSoonScreen | `app/src/screens/shared/ComingSoonScreen.tsx` | Placeholder |
|
|
|
|
---
|
|
|
|
## Chunking Guide (Claude Code Sessions)
|
|
|
|
### Chunk 1F: Bridge Package (start here — no dependencies)
|
|
|
|
**Goal:** Build `packages/webview-bridge/` from scratch.
|
|
|
|
**Steps:**
|
|
1. Delete `packages/webview-bridge/` if it exists
|
|
2. Create package structure (package.json, tsconfig, tsup.config)
|
|
3. Implement `types.ts` — all protocol types
|
|
4. Implement `bridge.ts` — WebViewBridge class
|
|
5. Implement `schema.ts` — validation
|
|
6. Implement `mock.ts` — MockNativeBridge
|
|
7. Implement all adapters in `adapters/`
|
|
8. Write tests
|
|
9. Validate: `npm run build && npx vitest run`
|
|
|
|
**Estimated effort:** This is the most self-contained chunk. All interfaces are defined in the spec above and in `packages/mobile-sdk-alpha/src/types/public.ts`.
|
|
|
|
### Chunk 1B-1D: Screens (after bridge, can be parallel)
|
|
|
|
**Goal:** Build all screen components in `packages/webview-app/src/screens/`.
|
|
|
|
Each screen should:
|
|
- Use Tamagui components (`Text`, `View`, `YStack`, `XStack`, `ScrollView`, `Button`, `Spinner`)
|
|
- Import colors/fonts from `@selfxyz/mobile-sdk-alpha/constants`
|
|
- Access SDK via `useSelfClient()` and `useBridge()` hooks
|
|
- Use `useNavigate()` from `react-router-dom` for navigation
|
|
- Reference the corresponding RN app screen for UI fidelity
|
|
|
|
### Chunk 1E: WebView App Shell (after bridge + screens)
|
|
|
|
**Goal:** Wire everything together in `packages/webview-app/`.
|
|
|
|
**Steps:**
|
|
1. Delete `packages/webview-app/` if it exists
|
|
2. Create package structure (package.json, vite.config, tamagui.config, index.html)
|
|
3. Copy fonts into `public/fonts/`
|
|
4. Create `main.tsx`, `App.tsx`, `fonts.css`, `reset.css`
|
|
5. Create `BridgeProvider.tsx`, `SelfClientProvider.tsx`
|
|
6. Wire all screens with React Router
|
|
7. Validate: `npx vite dev` serves the app, `npx vite build` produces `dist/`
|
|
|
|
---
|
|
|
|
## Important Notes
|
|
|
|
1. **No `react-native` dependency in bridge package.** The bridge is pure TypeScript, works in any browser.
|
|
2. **`react-native-web` is only in webview-app.** The Vite alias maps `react-native` → `react-native-web`.
|
|
3. **Fonts are inlined by Vite** when < 100KB (`assetsInlineLimit: 102400`). This means the built HTML+JS is self-contained.
|
|
4. **`mobile-sdk-alpha` is a workspace dependency.** Its `constants/colors.ts` and `constants/fonts.ts` are used directly (but `fonts.ts` imports `Platform` from react-native, so webview-app needs the `react-native-web` alias).
|
|
5. **The `SelfClientProvider` should eventually call `createSelfClient(adapters)`** from `mobile-sdk-alpha` once that function is available. For now, expose individual adapters directly (matching the prototype pattern).
|