Add IDDataScreen and ManageDocumentsScreen (WV-14) (#1878)

* feat(webview-app): add IDDataScreen and ManageDocumentsScreen (SELF-2422)

Screen migration for WV-14. Adds 2 euclid screen wrappers with mock data for the UI mocking pass.

- IDDataScreen at /id-data with ExposedIDCard, identification details, document data
- ManageDocumentsScreen at /manage-documents with document list and manage dialogue
- Wire Settings > Manage Documents to /manage-documents instead of /coming-soon
- Add preview.html for phone-frame screen verification during development

* update

* rename

---------

Co-authored-by: Tranquil-Flow <tranquil_flow@protonmail.com>
This commit is contained in:
Justin Hernandez
2026-03-28 17:48:44 -07:00
committed by GitHub
parent ba2e598fbe
commit 80488dd5c0
6 changed files with 367 additions and 2 deletions

View File

@@ -15,6 +15,8 @@ import { SettingsScreen } from './screens/account/SettingsScreen';
import { ComingSoonScreen } from './screens/ComingSoonScreen';
import { KeychainDebugScreen } from './screens/debug/KeychainDebugScreen';
import { HomeScreen } from './screens/home/HomeScreen';
import { IDDataScreen } from './screens/home/IDDataScreen';
import { ManageDocumentsScreen } from './screens/home/ManageDocumentsScreen';
import { ConfirmIdentificationScreen } from './screens/onboarding/ConfirmIdentificationScreen';
import { ConflictDetectedScreen } from './screens/onboarding/ConflictDetectedScreen';
import { CountryPickerScreen } from './screens/onboarding/CountryPickerScreen';
@@ -83,6 +85,8 @@ export const App: React.FC = () => (
<Route path="/proving/kyc-pending" element={<KycPendingScreen />} />
<Route path="/proving/kyc-success" element={<KycSuccessScreen />} />
<Route path="/account/verified" element={<VerificationResultScreen />} />
<Route path="/id-data" element={<IDDataScreen />} />
<Route path="/manage-documents" element={<ManageDocumentsScreen />} />
<Route path="/coming-soon" element={<ComingSoonScreen />} />
<Route path="/tunnel/tour/:step" element={<TunnelTourScreen />} />
<Route path="/tunnel/kyc" element={<KycMockScreen />} />

View File

@@ -8,6 +8,8 @@ import { useLocation, useNavigate } from 'react-router-dom';
const mockScreenLinks = [
{ href: '/settings/dev-mode', label: 'Dev Mode' },
{ href: '/manage-documents', label: 'Manage Documents' },
{ href: '/id-data', label: 'ID Data' },
{ href: '/proving/receipt', label: 'Proof Receipt' },
{ href: '/proving/history', label: 'Proof History' },
{ href: '/proving/dialogue', label: 'Simple Dialogue' },
@@ -67,7 +69,7 @@ export const DevRouteMenu: React.FC = () => {
textTransform: 'uppercase',
}}
>
WV-13 Mock Screens
Mock Screens
</div>
{mockScreenLinks.map(link => {
const isActive = location.pathname === link.href;

View File

@@ -53,7 +53,7 @@ export const SettingsScreen: React.FC = () => {
icon: DocumentDetailsIcon,
label: 'Manage Documents',
description: 'Recovery phrase, passport data',
onPress: () => navigate('/coming-soon'),
onPress: () => navigate('/manage-documents'),
},
{
icon: LockIcon,

View File

@@ -0,0 +1,83 @@
// 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 type React from 'react';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import {
IdCardIcon,
IDDataScreen as EuclidIDDataScreen,
LeftArrowIcon,
QuestionCircleStrokeIcon,
} from '@selfxyz/euclid';
import { useSelfClient } from '../../providers/SelfClientProvider';
import { WEB_SAFE_AREA } from '../../utils/insets';
const MOCK_ID_CARD_DETAILS = {
profileImage: '',
type: 'ID CARD',
code: 'SELF',
documentNumber: '••••••1234',
surname: 'DOE',
givenName: 'JOHN',
sex: 'M',
nationality: 'UNITED STATES',
dateOfBirth: '1990-01-15',
placeOfBirth: 'NEW YORK',
dateOfIssue: '2020-01-15',
dateOfExpiry: '2030-01-15',
};
const MOCK_DOCUMENT_DATA = [
{ label: 'ID Type', value: 'Passport' },
{ label: 'Document number', value: '18-299217823' },
{ label: 'Surname', value: 'Doe' },
{ label: 'Given name', value: 'John' },
{ label: 'Nationality', value: 'United States' },
{ label: 'Date of birth', value: '1990-01-15' },
];
export const IDDataScreen: React.FC = () => {
const navigate = useNavigate();
const { analytics, haptic } = useSelfClient();
const onClose = useCallback(() => {
haptic.trigger('selection');
navigate(-1);
}, [navigate, haptic]);
const onManageID = useCallback(() => {
haptic.trigger('selection');
analytics.trackEvent('id_data_manage_pressed');
navigate('/manage-documents');
}, [navigate, haptic, analytics]);
return (
<EuclidIDDataScreen
insets={WEB_SAFE_AREA.insets}
idCard={{
title: 'Passport',
subtitleLine1: 'UNITED STATES PASSPORT',
details: MOCK_ID_CARD_DETAILS,
mrzLine1: 'P<USA0000000000USA9001150M3001150<<<<<<<<<<<<<<',
mrzLine2: 'DOE<<JOHN<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<0',
}}
identificationDetailsTitle="Identification details"
identificationDetailsDescription="All data is stored locally on your device. Self does not collect or share any of this information without your consent."
identificationDetailsLogo={
<div style={{ width: 32, height: 21, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<IdCardIcon size={24} color="#2563EB" />
</div>
}
documentData={MOCK_DOCUMENT_DATA}
onClose={onClose}
onInfo={() => analytics.trackEvent('id_data_info_pressed')}
onManageID={onManageID}
closeIcon={({ size, color }) => <LeftArrowIcon size={size} color={color} />}
infoIcon={({ size, color }) => <QuestionCircleStrokeIcon size={size} color={color} />}
/>
);
};

View File

@@ -0,0 +1,77 @@
// 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 type React from 'react';
import { useCallback, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { LeftArrowIcon, ManageDocumentsScreen as EuclidManageDocumentsScreen, PlusIcon } from '@selfxyz/euclid';
import { useSelfClient } from '../../providers/SelfClientProvider';
import { WEB_SAFE_AREA } from '../../utils/insets';
export const ManageDocumentsScreen: React.FC = () => {
const navigate = useNavigate();
const { analytics, haptic } = useSelfClient();
const [dialogue, setDialogue] = useState<{ title: string; description: string } | undefined>();
const onBack = useCallback(() => {
haptic.trigger('selection');
navigate('/settings');
}, [navigate, haptic]);
const onAddDocument = useCallback(() => {
haptic.trigger('selection');
analytics.trackEvent('manage_docs_add_pressed');
navigate('/onboarding/country');
}, [navigate, haptic, analytics]);
const onDocumentPress = useCallback(() => {
haptic.trigger('selection');
setDialogue({
title: 'Manage Document',
description: 'View details or remove this document from your Self ID.',
});
}, [haptic]);
const onViewIdDetails = useCallback(() => {
haptic.trigger('selection');
analytics.trackEvent('manage_docs_view_details');
setDialogue(undefined);
navigate('/id-data');
}, [navigate, haptic, analytics]);
const onRemoveId = useCallback(() => {
haptic.trigger('warning');
analytics.trackEvent('manage_docs_remove_pressed');
setDialogue(undefined);
}, [haptic, analytics]);
const onDismissDialogue = useCallback(() => {
haptic.trigger('selection');
setDialogue(undefined);
}, [haptic]);
return (
<EuclidManageDocumentsScreen
insets={WEB_SAFE_AREA.insets}
escapeIcon={({ size, color }) => <LeftArrowIcon size={size} color={color} />}
addIcon={({ size, color }) => <PlusIcon size={size} color={color} />}
documents={[
{
id: 'mock-passport',
label: 'Passport',
description: 'Registered',
onPress: onDocumentPress,
},
]}
onBack={onBack}
onAddDocument={onAddDocument}
dialogue={dialogue}
onViewIdDetails={onViewIdDetails}
onRemoveId={onRemoveId}
onDismissDialogue={onDismissDialogue}
/>
);
};

View File

@@ -0,0 +1,199 @@
// 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.
// @vitest-environment jsdom
import type React from 'react';
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { DevRouteMenu } from '../../../src/components/DevRouteMenu';
import { SettingsScreen } from '../../../src/screens/account/SettingsScreen';
import { HomeScreen } from '../../../src/screens/home/HomeScreen';
import { IDDataScreen } from '../../../src/screens/home/IDDataScreen';
import { ManageDocumentsScreen } from '../../../src/screens/home/ManageDocumentsScreen';
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
const analytics = { trackEvent: vi.fn() };
const haptic = { trigger: vi.fn() };
const lifecycle = { dismiss: vi.fn() };
const documents = { loadDocumentCatalog: vi.fn() };
vi.mock('../../../src/providers/SelfClientProvider', () => ({
useSelfClient: () => ({
analytics,
haptic,
lifecycle,
documents,
}),
}));
vi.mock('@selfxyz/euclid', () => ({
createSafeAreaProps: ({ top, bottom }: { top: number; bottom: number }) => ({
insets: { top, bottom, left: 0, right: 0 },
safeArea: { top, bottom, left: 0, right: 0 },
}),
GearIcon: () => null,
LeftArrowIcon: () => null,
PlusIcon: () => null,
IdCardIcon: () => null,
QuestionCircleStrokeIcon: () => null,
DocumentDetailsIcon: () => null,
LockIcon: () => null,
NotificationIcon: () => null,
ChatStrokeIcon: () => null,
ShareIcon: () => null,
CodeIcon: () => null,
HomeScreen: ({
idCard,
onAddIdPress,
topNavigationPrimaryButton,
}: {
idCard?: { title: string; subtitle: string };
onAddIdPress: () => void;
topNavigationPrimaryButton: { onPress: () => void };
}) => (
<div>
{idCard ? <div>{`${idCard.title} ${idCard.subtitle}`}</div> : <div>No document</div>}
<button onClick={onAddIdPress} type="button">
Add ID
</button>
<button onClick={topNavigationPrimaryButton.onPress} type="button">
Open settings
</button>
</div>
),
SettingsViewScreen: ({ sections }: { sections: Array<{ items: Array<{ label: string; onPress: () => void }> }> }) => (
<div>
{sections.flatMap(section =>
section.items.map(item => (
<button key={item.label} onClick={item.onPress} type="button">
{item.label}
</button>
)),
)}
</div>
),
ManageDocumentsScreen: ({
documents: docs,
onViewIdDetails,
onDismissDialogue,
dialogue,
}: {
documents: Array<{ id: string; label: string; onPress: () => void }>;
onViewIdDetails: () => void;
onDismissDialogue: () => void;
dialogue?: { title: string };
}) => (
<div>
{docs.map(doc => (
<button key={doc.id} onClick={doc.onPress} type="button">
{doc.label}
</button>
))}
{dialogue ? (
<div>
<div>{dialogue.title}</div>
<button onClick={onViewIdDetails} type="button">
View details
</button>
<button onClick={onDismissDialogue} type="button">
Close dialog
</button>
</div>
) : null}
</div>
),
IDDataScreen: ({ onManageID, onClose }: { onManageID: () => void; onClose: () => void }) => (
<div>
<button onClick={onManageID} type="button">
Manage ID
</button>
<button onClick={onClose} type="button">
Close ID data
</button>
</div>
),
}));
const LocationDisplay: React.FC = () => {
const location = useLocation();
return <div data-testid="location">{location.pathname}</div>;
};
const renderRoutes = (initialEntries: string[]) =>
render(
<MemoryRouter initialEntries={initialEntries}>
<Routes>
<Route path="/" element={<HomeScreen />} />
<Route path="/settings" element={<SettingsScreen />} />
<Route path="/manage-documents" element={<ManageDocumentsScreen />} />
<Route path="/id-data" element={<IDDataScreen />} />
<Route path="/settings/dev-mode" element={<LocationDisplay />} />
</Routes>
<DevRouteMenu />
<LocationDisplay />
</MemoryRouter>,
);
describe('WV-14 support screens', () => {
beforeEach(() => {
vi.clearAllMocks();
documents.loadDocumentCatalog.mockResolvedValue({
documents: [
{
id: 'doc-1',
documentType: 'p',
documentCategory: 'passport',
data: '{}',
mock: true,
isRegistered: true,
},
],
});
sessionStorage.clear();
});
afterEach(() => {
cleanup();
});
it('stitches home to settings, manage documents, and ID data', async () => {
renderRoutes(['/']);
await waitFor(() => {
expect(screen.getByRole('button', { name: /open settings/i })).toBeTruthy();
});
fireEvent.click(screen.getByRole('button', { name: /open settings/i }));
expect(screen.getByTestId('location').textContent).toBe('/settings');
fireEvent.click(screen.getByRole('button', { name: /manage documents/i }));
expect(screen.getByTestId('location').textContent).toBe('/manage-documents');
fireEvent.click(screen.getByRole('button', { name: /passport/i }));
fireEvent.click(screen.getByRole('button', { name: /view details/i }));
expect(screen.getByTestId('location').textContent).toBe('/id-data');
fireEvent.click(screen.getByRole('button', { name: /manage id/i }));
expect(screen.getByTestId('location').textContent).toBe('/manage-documents');
});
it('exposes manage documents and ID data in the dev route menu', async () => {
renderRoutes(['/']);
await waitFor(() => {
expect(screen.getByRole('button', { name: /open settings/i })).toBeTruthy();
});
fireEvent.click(screen.getByRole('button', { name: /mock screens/i }));
expect(screen.getByRole('button', { name: 'Manage Documents' })).toBeTruthy();
expect(screen.getByRole('button', { name: 'ID Data' })).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'ID Data' }));
expect(screen.getByTestId('location').textContent).toBe('/id-data');
});
});