mirror of
https://github.com/selfxyz/self.git
synced 2026-04-27 03:01:15 -04:00
wv-01 work (#1844)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user