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.