diff --git a/packages/webview-app/src/App.tsx b/packages/webview-app/src/App.tsx index 7e94fc908..491309c8a 100644 --- a/packages/webview-app/src/App.tsx +++ b/packages/webview-app/src/App.tsx @@ -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 = () => ( + } /> @@ -40,5 +42,6 @@ export const App: React.FC = () => ( } /> + ); diff --git a/packages/webview-app/src/providers/VerificationRequestProvider.tsx b/packages/webview-app/src/providers/VerificationRequestProvider.tsx new file mode 100644 index 000000000..495658a95 --- /dev/null +++ b/packages/webview-app/src/providers/VerificationRequestProvider.tsx @@ -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(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 {children}; +}; diff --git a/packages/webview-app/src/screens/proving/ProvingScreen.tsx b/packages/webview-app/src/screens/proving/ProvingScreen.tsx index 8b4815f13..df258fca6 100644 --- a/packages/webview-app/src/screens/proving/ProvingScreen.tsx +++ b/packages/webview-app/src/screens/proving/ProvingScreen.tsx @@ -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} /> ); }; diff --git a/specs/projects/sdk/workstreams/webview/SPEC.md b/specs/projects/sdk/workstreams/webview/SPEC.md index fdd282694..3a6ebfc75 100644 --- a/specs/projects/sdk/workstreams/webview/SPEC.md +++ b/specs/projects/sdk/workstreams/webview/SPEC.md @@ -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 diff --git a/specs/projects/sdk/workstreams/webview/plans/WV-01-dynamic-proof-request-items.md b/specs/projects/sdk/workstreams/webview/plans/WV-01-dynamic-proof-request-items.md index 4ebae1b48..811ff7f42 100644 --- a/specs/projects/sdk/workstreams/webview/plans/WV-01-dynamic-proof-request-items.md +++ b/specs/projects/sdk/workstreams/webview/plans/WV-01-dynamic-proof-request-items.md @@ -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.