mirror of
https://github.com/selfxyz/self.git
synced 2026-04-27 03:01:15 -04:00
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:
@@ -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 />} />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
83
packages/webview-app/src/screens/home/IDDataScreen.tsx
Normal file
83
packages/webview-app/src/screens/home/IDDataScreen.tsx
Normal 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} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user