wv-01 work (#1844)

This commit is contained in:
Justin Hernandez
2026-03-11 14:09:16 -07:00
committed by GitHub
parent 929ef3832e
commit 25e8ddda37
5 changed files with 128 additions and 108 deletions

View File

@@ -5,6 +5,7 @@
import React from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { SelfClientProvider } from './providers/SelfClientProvider';
import { VerificationRequestProvider } from './providers/VerificationRequestProvider';
import { CountryPickerScreen } from './screens/onboarding/CountryPickerScreen';
import { IDSelectionScreen } from './screens/onboarding/IDSelectionScreen';
import { DocumentCameraScreen } from './screens/onboarding/DocumentCameraScreen';
@@ -18,6 +19,7 @@ import { ComingSoonScreen } from './screens/ComingSoonScreen';
export const App: React.FC = () => (
<BrowserRouter>
<VerificationRequestProvider>
<SelfClientProvider>
<Routes>
<Route path="/" element={<HomeScreen />} />
@@ -40,5 +42,6 @@ export const App: React.FC = () => (
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</SelfClientProvider>
</VerificationRequestProvider>
</BrowserRouter>
);

View File

@@ -0,0 +1,107 @@
// 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 React, { createContext, useContext, useMemo } from 'react';
import type { VerificationRequest } from '@selfxyz/mobile-sdk-alpha';
export interface VerificationRequestContext {
/** Parsed verification request from URL params. */
request: VerificationRequest;
/** Optional display-label overrides from the host (proofItems param). */
displayLabels: string[] | null;
/** Display name for the requesting application. */
appName: string;
/** Sanitized host/endpoint string for the requesting application. */
appEndpoint: string;
/** Timestamp of the request (epoch ms). */
timestamp: number;
/** The request type (e.g. 'proofRequested'). */
requestType: string;
}
const ALLOWED_REQUEST_TYPES = new Set([
'proofRequested',
'documentOwnershipConfirmed',
]);
const DEFAULT_REQUEST_TYPE = 'proofRequested';
const Ctx = createContext<VerificationRequestContext | null>(null);
export function useVerificationRequest(): VerificationRequestContext {
const ctx = useContext(Ctx);
if (!ctx) {
throw new Error(
'useVerificationRequest must be used within a VerificationRequestProvider',
);
}
return ctx;
}
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 splitCSV(value: string): string[] {
return value.split(',').map((s) => s.trim()).filter(Boolean);
}
function parseDisclosures(params: URLSearchParams): string[] | undefined {
const raw = params.get('disclosures');
if (!raw) return undefined;
const items = splitCSV(raw);
return items.length > 0 ? items : undefined;
}
function parseDisplayLabels(params: URLSearchParams): string[] | null {
const raw = params.get('proofItems');
if (!raw) return null;
const items = splitCSV(raw);
return items.length > 0 ? items : null;
}
function parseFromURL(): VerificationRequestContext {
const params = new URLSearchParams(window.location.search);
const request: VerificationRequest = {
userId: params.get('userId') ?? undefined,
scope: params.get('scope') ?? undefined,
disclosures: parseDisclosures(params),
};
const queryTimestamp = params.get('timestamp');
const parsed = queryTimestamp ? Number(queryTimestamp) : Number.NaN;
return {
request,
displayLabels: parseDisplayLabels(params),
appName: params.get('appName') ?? 'Verification',
appEndpoint: normalizeAppEndpoint(params.get('appEndpoint')),
timestamp: Number.isFinite(parsed) ? parsed : Date.now(),
requestType: normalizeRequestType(params.get('resultType')),
};
}
export const VerificationRequestProvider: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
const value = useMemo(() => parseFromURL(), []);
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
};

View File

@@ -3,29 +3,11 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback, useMemo, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { 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;
}
import { useVerificationRequest } from '../../providers/VerificationRequestProvider';
function titleCaseDisclosure(disclosure: string): string {
return disclosure
@@ -35,94 +17,21 @@ function titleCaseDisclosure(disclosure: string): string {
.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 { request, displayLabels, requestType, appName, appEndpoint, timestamp } =
useVerificationRequest();
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;
if (displayLabels && displayLabels.length > 0) {
return displayLabels.map((label) => ({ label }));
}
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]);
return (request.disclosures ?? []).map((key) => ({
label: titleCaseDisclosure(key),
}));
}, [displayLabels, request.disclosures]);
const onVerify = useCallback(async () => {
haptic.trigger('selection');
@@ -161,7 +70,7 @@ export const ProvingScreen: React.FC = () => {
appName={appName}
appEndpoint={appEndpoint}
timestamp={timestamp}
items={proofItems.map((label) => ({ label }))}
items={proofItems}
/>
);
};

View File

@@ -46,7 +46,7 @@ On **March 11, 2026**, the active SDK scope changed to **WebView only, with no c
| ID | Title | Status | Priority | Depends On | Plan | Notes |
| ----- | ----------------------------------------------------------------------------------------------- | ------ | -------- | ---------- | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| WV-01 | Dynamic proof request items sourced from request context | Ready | High | - | [plans/WV-01-dynamic-proof-request-items.md](./plans/WV-01-dynamic-proof-request-items.md) | Existing active follow-up |
| WV-01 | Dynamic proof request items sourced from request context | Done | High | - | [plans/WV-01-dynamic-proof-request-items.md](./plans/WV-01-dynamic-proof-request-items.md) | Existing active follow-up |
| WV-02 | Define the KYC-provider contract for document capture, MRZ/liveness handoff, and result mapping | Ready | High | - | [plans/WV-02-kyc-provider-contract.md](./plans/WV-02-kyc-provider-contract.md) | Provider-backed path replaces Self-owned native scan flow |
| WV-03 | Remove native NFC and native-scan assumptions from active WebView screens, copy, and docs | Ready | High | WV-02 | - | Active UX/docs should match the WebView-only scope |
| WV-04 | Define the host callback contract for launch, dismiss, and final result without native modules | Ready | Medium | WV-02 | - | Build on existing `SdkInitialConfig` and `VERIFICATION_COMPLETE` work; define only the WebView-host transport and embedding delta |
@@ -57,7 +57,7 @@ Allowed statuses: `Ready`, `In Progress`, `Blocked`, `Deferred`, `Done`
| Plan | IDs | Status |
| ------------------------------------------------------------------------------------------ | ----- | ------ |
| [plans/WV-01-dynamic-proof-request-items.md](./plans/WV-01-dynamic-proof-request-items.md) | WV-01 | Ready |
| [plans/WV-01-dynamic-proof-request-items.md](./plans/WV-01-dynamic-proof-request-items.md) | WV-01 | Done |
| [plans/WV-02-kyc-provider-contract.md](./plans/WV-02-kyc-provider-contract.md) | WV-02 | Ready |
## Completion Checklist

View File

@@ -1,7 +1,7 @@
# Dynamic Proof Request Items
> Last updated: 2026-03-10
> Status: Ready
> Status: Done
- Workstream: webview
- Backlog IDs: WV-01
@@ -48,10 +48,11 @@ cd packages/mobile-sdk-alpha && npx vitest run
## Definition of Done
- [ ] Proof request items are no longer hardcoded
- [ ] Request-context rendering is validated
- [ ] Backlog row updated
- [x] Proof request items are no longer hardcoded
- [x] Request-context rendering is validated
- [x] Backlog row updated
## Status Log
- 2026-03-10: Created during spec cleanup.
- 2026-03-11: Implemented. Created `VerificationRequestProvider` to parse URL params into a typed `VerificationRequest` context. Refactored `ProvingScreen` to consume the context instead of hardcoding `DEFAULT_PROOF_ITEMS`. Both validation commands pass.