From 8dc1720c954a5a373270fbad2332bd34a2874c7b Mon Sep 17 00:00:00 2001 From: macbook Date: Fri, 31 Jul 2026 12:02:08 +0100 Subject: [PATCH 01/10] Add verification wizard tests and review improvements --- frontend/components/ui/tooltip.tsx | 96 ++++++++++++++++ .../__tests__/wizard-flow.test.tsx | 107 +++++++++++++++++ .../components/WizardPageShell.tsx | 4 +- .../components/steps/ReviewSubmitStep.tsx | 4 + .../components/steps/SPVPrivacyStep.tsx | 75 ++++++++++-- .../components/steps/UploadReview.tsx | 63 ++++++++-- .../steps/__tests__/SPVPrivacyStep.test.tsx | 108 +++++++++--------- 7 files changed, 378 insertions(+), 79 deletions(-) create mode 100644 frontend/components/ui/tooltip.tsx create mode 100644 frontend/features/verification/__tests__/wizard-flow.test.tsx create mode 100644 frontend/features/verification/components/steps/ReviewSubmitStep.tsx diff --git a/frontend/components/ui/tooltip.tsx b/frontend/components/ui/tooltip.tsx new file mode 100644 index 0000000..4851092 --- /dev/null +++ b/frontend/components/ui/tooltip.tsx @@ -0,0 +1,96 @@ +'use client'; + +import * as React from 'react'; + +type TooltipContextValue = { + open: boolean; + setOpen: (open: boolean) => void; +}; + +const TooltipContext = React.createContext(null); + +function useTooltipContext() { + const context = React.useContext(TooltipContext); + if (!context) { + throw new Error('Tooltip components must be used within .'); + } + return context; +} + +export function TooltipProvider({ children }: { children: React.ReactNode }) { + return <>{children}; +} + +export function Tooltip({ children }: { children: React.ReactNode }) { + const [open, setOpen] = React.useState(false); + + return ( + + {children} + + ); +} + +export function TooltipTrigger({ + asChild, + children, +}: { + asChild?: boolean; + children: React.ReactElement; +}) { + const { setOpen } = useTooltipContext(); + const child = children as React.ReactElement>; + const childProps = child.props as { + onMouseEnter?: (event: React.MouseEvent) => void; + onMouseLeave?: (event: React.MouseEvent) => void; + onFocus?: (event: React.FocusEvent) => void; + onBlur?: (event: React.FocusEvent) => void; + }; + + return React.cloneElement(child, { + onMouseEnter: (event: React.MouseEvent) => { + childProps.onMouseEnter?.(event); + setOpen(true); + }, + onMouseLeave: (event: React.MouseEvent) => { + childProps.onMouseLeave?.(event); + setOpen(false); + }, + onFocus: (event: React.FocusEvent) => { + childProps.onFocus?.(event); + setOpen(true); + }, + onBlur: (event: React.FocusEvent) => { + childProps.onBlur?.(event); + setOpen(false); + }, + ...(asChild ? {} : { type: 'button' }), + }); +} + +export function TooltipContent({ + className = '', + children, +}: { + className?: string; + children: React.ReactNode; + side?: 'top' | 'bottom' | 'left' | 'right'; + sideOffset?: number; +}) { + const { open } = useTooltipContext(); + + if (!open) return null; + + return ( + + {children} + + ); +} diff --git a/frontend/features/verification/__tests__/wizard-flow.test.tsx b/frontend/features/verification/__tests__/wizard-flow.test.tsx new file mode 100644 index 0000000..9a81922 --- /dev/null +++ b/frontend/features/verification/__tests__/wizard-flow.test.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import WizardPageShell from '../components/WizardPageShell'; +import { useWizardStore } from '../store/wizard.store'; + +jest.mock('@/utils/hashing', () => ({ + hashFile: jest.fn().mockResolvedValue('a'.repeat(64)), +})); + +jest.mock('@/utils/crypto', () => ({ + computeSHA256: jest.fn().mockResolvedValue('b'.repeat(64)), + isValidSHA256: jest.fn((value: string) => /^[a-f0-9]{64}$/i.test(value)), +})); + +jest.mock('@/context/WalletContext', () => ({ + useWallet: jest.fn(() => ({ + publicKey: null, + isConnected: false, + connect: jest.fn(), + })), +})); + +jest.mock('@/app/context/AuthContext', () => ({ + useAuth: jest.fn(() => ({ + user: { + name: 'Test Creator', + email: 'creator@example.com', + }, + })), +})); + +jest.mock('@/components/ManifestGeneratorModal', () => ({ + __esModule: true, + default: () => null, +})); + +jest.mock('@/services/manifestUseCases', () => ({ + manifestUseCaseService: { + getAll: jest.fn().mockResolvedValue([]), + }, +})); + +jest.mock('@/services/verificationService', () => ({ + submitVerificationRequest: jest.fn(), +})); + +describe('Verification wizard flow', () => { + beforeAll(() => { + global.URL.createObjectURL = jest.fn(() => 'blob:preview'); + global.URL.revokeObjectURL = jest.fn(); + }); + + beforeEach(() => { + useWizardStore.setState({ + currentStep: 0, + formData: {}, + validation: {}, + _hasHydrated: true, + }); + }); + + it('navigates through all four steps and carries data into the review summary', async () => { + const user = userEvent.setup(); + const { container } = render(); + + const mediaInput = container.querySelector('input[type="file"]') as HTMLInputElement; + const mediaFile = new File(['image-bytes'], 'asset.png', { type: 'image/png' }); + await user.upload(mediaInput, mediaFile); + + await waitFor(() => { + expect(screen.getByText(/sha-256 hash/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /go to next step/i })).toBeEnabled(); + }); + + await user.click(screen.getByRole('button', { name: /go to next step/i })); + expect(screen.getByRole('heading', { name: /manifest attachment/i })).toBeInTheDocument(); + + const manifestInput = container.querySelectorAll('input[type="file"]')[0] as HTMLInputElement; + const manifestFile = new File(['{"name":"Asset Manifest"}'], 'manifest.json', { + type: 'application/json', + }); + await user.upload(manifestInput, manifestFile); + + await waitFor(() => { + expect(screen.getByText(/manifest sha-256 hash/i)).toBeInTheDocument(); + }); + + await user.click(screen.getByRole('button', { name: /go to next step/i })); + expect(screen.getByRole('heading', { name: /spv privacy/i })).toBeInTheDocument(); + + await user.click(screen.getByRole('radio', { name: /public registry/i })); + await user.click(screen.getByRole('button', { name: /go to next step/i })); + + expect(screen.getByRole('heading', { name: /review & submit/i })).toBeInTheDocument(); + expect(screen.getByText('asset.png')).toBeInTheDocument(); + expect(screen.getByText('manifest.json')).toBeInTheDocument(); + expect(screen.getByText(/^Public$/)).toBeInTheDocument(); + expect(screen.getByText('a'.repeat(64))).toBeInTheDocument(); + expect(screen.getByText('b'.repeat(64))).toBeInTheDocument(); + + const state = useWizardStore.getState(); + expect(state.formData.content?.file?.name).toBe('asset.png'); + expect(state.formData.content?.manifest?.fileName).toBe('manifest.json'); + expect(state.formData.content?.encryptionEnabled).toBe(false); + }); +}); diff --git a/frontend/features/verification/components/WizardPageShell.tsx b/frontend/features/verification/components/WizardPageShell.tsx index d160c03..eab0a58 100644 --- a/frontend/features/verification/components/WizardPageShell.tsx +++ b/frontend/features/verification/components/WizardPageShell.tsx @@ -8,7 +8,7 @@ import WizardNavigation from './WizardNavigation'; import UploadMedia from './steps/UploadMedia'; import UploadManifest from './steps/UploadManifest'; import SPVPrivacyStep from './steps/SPVPrivacyStep'; -import UploadReview from './steps/UploadReview'; +import ReviewSubmitStep from './steps/ReviewSubmitStep'; import { submitVerificationRequest } from '@/services/verificationService'; const STEPS = [ @@ -88,7 +88,7 @@ export default function WizardPageShell() { , , , - , nextValue: boolean) => { + if (event.key === ' ' || event.key === 'Enter') { + event.preventDefault(); + setEncryptionEnabled(nextValue); + } + }, + [setEncryptionEnabled], + ); + return ( -
+ +
{/* Intro info header */}

@@ -43,11 +67,12 @@ export default function SPVPrivacyStep() { className="grid grid-cols-1 md:grid-cols-2 gap-6" > {/* PUBLIC REGISTRY CARD */} - + + + {PRIVACY_TOOLTIPS.public} + +

Standard On-Chain Storage @@ -103,14 +143,15 @@ export default function SPVPrivacyStep() {

- + {/* KMS ENCRYPTED CARD */} - + + + {PRIVACY_TOOLTIPS.encrypted} + +

Sealed Provenance Vault @@ -166,7 +222,7 @@ export default function SPVPrivacyStep() { - + {/* Sync Toggle Switch & Description */} @@ -227,6 +283,7 @@ export default function SPVPrivacyStep() { )} - + + ); } diff --git a/frontend/features/verification/components/steps/UploadReview.tsx b/frontend/features/verification/components/steps/UploadReview.tsx index b07ee37..f2f75cf 100644 --- a/frontend/features/verification/components/steps/UploadReview.tsx +++ b/frontend/features/verification/components/steps/UploadReview.tsx @@ -6,6 +6,7 @@ import { Edit2, FileJson, ShieldCheck, + ShieldAlert, Hash, FileText, Send, @@ -13,6 +14,7 @@ import { Upload, Wallet, AlertCircle, + Image as ImageIcon, } from 'lucide-react'; import { useWizardStore } from '../../store/wizard.store'; import { isValidSHA256 } from '@/utils/crypto'; @@ -99,6 +101,9 @@ export default function UploadReview({ const [isConnecting, setIsConnecting] = useState(false); const modeName = content?.encryptionEnabled ? 'KMS Encrypted' : 'Public'; + const modeDescription = content?.encryptionEnabled + ? 'Restricted access through managed decryption keys.' + : 'Readable directly from the public registry for open verification.'; const contentHashValid = isValidSHA256(content?.contentHash ?? ''); const hasManifest = content?.manifest !== null && content?.manifest !== undefined; @@ -130,7 +135,25 @@ export default function UploadReview({

} title="Uploaded File" stepIndex={0} onNavigate={onNavigate} /> {content?.file ? ( - +
+ {content.file.previewUrl ? ( + /* eslint-disable-next-line @next/next/no-img-element */ + {`${content.file.name} + ) : ( +
+ +
+ )} +
+ + + +
+
) : (

No file uploaded.

)} @@ -138,16 +161,28 @@ export default function UploadReview({
} title="Verification Mode" /> - - {content?.encryptionEnabled && } - {modeName} - +
+
+ + {content?.encryptionEnabled ? : } + {modeName} + +

{modeDescription}

+
+ +
@@ -168,7 +203,11 @@ export default function UploadReview({
} title="Manifest" stepIndex={1} onNavigate={onNavigate} /> {hasManifest && content?.manifestHash ? ( - +
+ + + +
) : (

No manifest attached.

)} diff --git a/frontend/features/verification/components/steps/__tests__/SPVPrivacyStep.test.tsx b/frontend/features/verification/components/steps/__tests__/SPVPrivacyStep.test.tsx index 8a2b2ea..d94c546 100644 --- a/frontend/features/verification/components/steps/__tests__/SPVPrivacyStep.test.tsx +++ b/frontend/features/verification/components/steps/__tests__/SPVPrivacyStep.test.tsx @@ -4,83 +4,79 @@ import userEvent from '@testing-library/user-event'; import SPVPrivacyStep from '../SPVPrivacyStep'; import { useWizardStore } from '../../../store/wizard.store'; +jest.mock('../../../store/wizard.store', () => ({ + useWizardStore: jest.fn(), +})); + +type MockWizardState = { + formData: { + content: { + encryptionEnabled: boolean; + }; + }; + setEncryptionEnabled: jest.Mock; + setStepValid: jest.Mock; +}; + describe('SPVPrivacyStep', () => { + const mockUseWizardStore = useWizardStore as unknown as jest.Mock; + let mockState: MockWizardState; + beforeEach(() => { - // Reset store state before each test - useWizardStore.setState({ + mockState = { formData: { content: { - file: null, - contentHash: null, - hashProgress: 0, - isHashing: false, - manifest: null, - manifestHash: null, encryptionEnabled: true, - spvResult: null, }, }, - validation: {}, - }); + setEncryptionEnabled: jest.fn((enabled: boolean) => { + mockState.formData.content.encryptionEnabled = enabled; + }), + setStepValid: jest.fn(), + }; + + mockUseWizardStore.mockImplementation((selector?: (state: MockWizardState) => unknown) => + selector ? selector(mockState) : mockState, + ); }); - it('renders correctly with default state (KMS Encrypted = true)', () => { - render(); - - // Verify option cards exist - const publicCard = screen.getByRole('radio', { name: /Public Registry/i }); - const encryptedCard = screen.getByRole('radio', { name: /KMS Encrypted/i }); - - expect(publicCard).toBeInTheDocument(); - expect(encryptedCard).toBeInTheDocument(); - - // Verify correct aria-checked states based on initial state - expect(publicCard).toHaveAttribute('aria-checked', 'false'); - expect(encryptedCard).toHaveAttribute('aria-checked', 'true'); - - // Verify success banner is shown - expect(screen.getByText(/Safe Provenance Active/i)).toBeInTheDocument(); + afterEach(() => { + jest.clearAllMocks(); }); - it('toggles to public registry when clicking public card', async () => { - const user = userEvent.setup(); + it('renders with KMS Encrypted selected and marks the step valid', () => { render(); - const publicCard = screen.getByRole('radio', { name: /Public Registry/i }); - const encryptedCard = screen.getByRole('radio', { name: /KMS Encrypted/i }); - - // Click on public card - await user.click(publicCard); + expect(screen.getByRole('radio', { name: /public registry/i })).toHaveAttribute('aria-checked', 'false'); + expect(screen.getByRole('radio', { name: /kms encrypted/i })).toHaveAttribute('aria-checked', 'true'); + expect(mockState.setStepValid).toHaveBeenCalledWith(2, true); + }); - // Verify aria-checked updates - expect(publicCard).toHaveAttribute('aria-checked', 'true'); - expect(encryptedCard).toHaveAttribute('aria-checked', 'false'); + it('updates the mocked store when a user selects the public option', async () => { + const user = userEvent.setup(); + const { rerender } = render(); - // Verify store state is updated - expect(useWizardStore.getState().formData.content?.encryptionEnabled).toBe(false); + await user.click(screen.getByRole('radio', { name: /public registry/i })); + rerender(); - // Verify warning callout shows up instead of success alert - expect(screen.getByText(/Plaintext Storage Warning/i)).toBeInTheDocument(); - expect(screen.queryByText(/Safe Provenance Active/i)).not.toBeInTheDocument(); + expect(mockState.setEncryptionEnabled).toHaveBeenCalledWith(false); + expect(screen.getByRole('radio', { name: /public registry/i })).toHaveAttribute('aria-checked', 'true'); + expect(screen.getByText(/plaintext storage warning/i)).toBeInTheDocument(); }); - it('toggles using the switch component', async () => { + it('shows interactive tooltip explanations for both privacy options', async () => { const user = userEvent.setup(); render(); - const toggle = screen.getByRole('switch'); - expect(toggle).toHaveAttribute('aria-checked', 'true'); - - // Toggle off (changes to public) - await user.click(toggle); - - expect(toggle).toHaveAttribute('aria-checked', 'false'); - expect(useWizardStore.getState().formData.content?.encryptionEnabled).toBe(false); - - // Toggle back on (changes to encrypted) - await user.click(toggle); + await user.hover(screen.getByRole('button', { name: /explain public registry privacy option/i })); + expect( + await screen.findByText(/public keeps the provenance record readable on-chain/i), + ).toBeInTheDocument(); - expect(toggle).toHaveAttribute('aria-checked', 'true'); - expect(useWizardStore.getState().formData.content?.encryptionEnabled).toBe(true); + await user.unhover(screen.getByRole('button', { name: /explain public registry privacy option/i })); + await user.hover(screen.getByRole('button', { name: /explain kms encrypted privacy option/i })); + expect( + await screen.findByText(/kms encrypted seals the provenance payload before submission/i), + ).toBeInTheDocument(); }); }); From 8d277e5d62e7b6762adb0e9a4d639ff9993a50c8 Mon Sep 17 00:00:00 2001 From: macbook Date: Fri, 31 Jul 2026 12:08:14 +0100 Subject: [PATCH 02/10] build fix --- .../assets/components/AssetTable.tsx | 2 +- .../dashboard/Web2DashboardView.tsx | 2 -- .../__tests__/manifest-schema.test.ts | 23 ++++++++++++++----- frontend/services/verificationService.ts | 10 ++++---- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/frontend/app/dashboard/assets/components/AssetTable.tsx b/frontend/app/dashboard/assets/components/AssetTable.tsx index aef519d..6ec5fa7 100644 --- a/frontend/app/dashboard/assets/components/AssetTable.tsx +++ b/frontend/app/dashboard/assets/components/AssetTable.tsx @@ -150,7 +150,7 @@ export default function AssetTable({ const itemsPerPage = 10; const sortedAssets = useMemo(() => { - let sortableItems = [...assets]; + const sortableItems = [...assets]; if (sortConfig !== null) { sortableItems.sort((a, b) => { if (a[sortConfig.key] < b[sortConfig.key]) { diff --git a/frontend/components/dashboard/Web2DashboardView.tsx b/frontend/components/dashboard/Web2DashboardView.tsx index 38366e9..84380cf 100644 --- a/frontend/components/dashboard/Web2DashboardView.tsx +++ b/frontend/components/dashboard/Web2DashboardView.tsx @@ -57,8 +57,6 @@ export default function Web2DashboardView({ userEmail = "user@example.com", user const [certificates, setCertificates] = useState([]); const [isLoading, setIsLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(""); - const [pageSize, setPageSize] = useState(10); - const [currentPage, setCurrentPage] = useState(1); const [selectedInspectItem, setSelectedInspectItem] = useState<{ title: string; data: Record } | null>(null); useEffect(() => { diff --git a/frontend/features/verification/__tests__/manifest-schema.test.ts b/frontend/features/verification/__tests__/manifest-schema.test.ts index 697fce9..c79f89a 100644 --- a/frontend/features/verification/__tests__/manifest-schema.test.ts +++ b/frontend/features/verification/__tests__/manifest-schema.test.ts @@ -1,7 +1,18 @@ -import { manifestSchema, validateManifest } from '../schemas/manifest-schema'; +import { validateManifest } from '../schemas/manifest-schema'; + +type ValidManifestPayload = { + contentHash: string; + creator: string; + timestamp: string; + metadata: { + device: string; + location: string; + aiModel: string; + }; +}; describe('Manifest Schema Validation', () => { - const validPayload = { + const validPayload: ValidManifestPayload = { contentHash: 'sha256:d2a84f4b8b650937ec8f73cd8be2c74add5a911ba64df27458ed8229da804a26', creator: 'GA2C5RFPE6GCKIG3EQKTNIQ6PRRQIHIRDIUCAUKENRXCVZBD4T6K2K2H', timestamp: '2023-10-27T10:00:00Z', @@ -30,13 +41,13 @@ describe('Manifest Schema Validation', () => { const result = validateManifest(extendedPayload); expect(result.success).toBe(true); if (result.success) { - expect((result.data as any).extraField).toBe('some extra value'); + expect(result.data).toMatchObject({ extraField: 'some extra value' }); } }); describe('Invalid Payloads', () => { it('should fail if contentHash is missing', () => { - const { contentHash, ...invalidPayload } = validPayload; + const { contentHash: _contentHash, ...invalidPayload } = validPayload; const result = validateManifest(invalidPayload); expect(result.success).toBe(false); if (!result.success) { @@ -74,7 +85,7 @@ describe('Manifest Schema Validation', () => { describe('Form Submission Logic', () => { it('should simulate form submission failure with invalid schema', () => { - const formSubmit = (data: any) => { + const formSubmit = (data: unknown) => { const result = validateManifest(data); if (!result.success) { throw new Error('Validation failed'); @@ -87,7 +98,7 @@ describe('Manifest Schema Validation', () => { }); it('should simulate form submission success with valid schema', () => { - const formSubmit = (data: any) => { + const formSubmit = (data: unknown) => { const result = validateManifest(data); if (!result.success) { throw new Error('Validation failed'); diff --git a/frontend/services/verificationService.ts b/frontend/services/verificationService.ts index dd5e525..cdbe74c 100644 --- a/frontend/services/verificationService.ts +++ b/frontend/services/verificationService.ts @@ -92,7 +92,7 @@ export const submitVerificationRequest = async ( if (submittedTransaction.result_xdr) { certificateId = submittedTransaction.hash.substring(0, 8); } - } catch (_parseError) { + } catch { // Silent catch - certificate ID is optional } @@ -146,7 +146,7 @@ export interface VerificationRequest { certificateId?: string; } -export const getVerificationRequests = async (_publicKey: string): Promise => { +export const getVerificationRequests = async (): Promise => { try { return getMockRequests(); } catch (error) { @@ -170,10 +170,12 @@ function getMockRequests(): VerificationRequest[] { } export const checkVerificationStatus = async ( - _requestId: string, - _publicKey: string + requestId: string, + publicKey: string ): Promise => { try { + void requestId; + void publicKey; await new Promise((resolve) => setTimeout(resolve, 500)); const statuses: VerificationStatus[] = ['pending', 'verified', 'failed']; return statuses[Math.floor(Math.random() * statuses.length)]; From ec25b4e0e194fec9326d45f498df59cd5c9dc583 Mon Sep 17 00:00:00 2001 From: macbook Date: Fri, 31 Jul 2026 12:19:51 +0100 Subject: [PATCH 03/10] build fix --- .../dashboard/assets/components/AssetGrid.tsx | 58 ------------------- 1 file changed, 58 deletions(-) diff --git a/frontend/app/dashboard/assets/components/AssetGrid.tsx b/frontend/app/dashboard/assets/components/AssetGrid.tsx index 94c0779..14088b7 100644 --- a/frontend/app/dashboard/assets/components/AssetGrid.tsx +++ b/frontend/app/dashboard/assets/components/AssetGrid.tsx @@ -253,61 +253,3 @@ function AssetCard({ asset }: AssetCardProps) { ); } - -interface AssetGridProps { - /** Optional preloaded assets. When omitted, mock data is loaded. */ - assets?: Asset[]; -} - -export default function AssetGrid({ assets }: AssetGridProps) { - const [mockItems, setMockItems] = useState(null); - const [isLoading, setIsLoading] = useState(!assets); - - useEffect(() => { - if (assets) return; - - let cancelled = false; - - // Simulate asset retrieval until the service layer is wired in. - const timer = setTimeout(() => { - if (!cancelled) { - setMockItems(MOCK_ASSETS); - setIsLoading(false); - } - }, 400); - - return () => { - cancelled = true; - clearTimeout(timer); - }; - }, [assets]); - - const items = assets ?? mockItems; - - return ( -
-
-

- Assets -

- {items && items.length > 0 && ( - - {items.length} asset{items.length !== 1 ? "s" : ""} - - )} -
- - {isLoading ? ( - - ) : !items || items.length === 0 ? ( - - ) : ( -
- {items.map((asset) => ( - - ))} -
- )} -
- ); -} From 20e6899faad941280566ed5107e46a038138187a Mon Sep 17 00:00:00 2001 From: macbook Date: Fri, 31 Jul 2026 12:40:55 +0100 Subject: [PATCH 04/10] frontend build fix --- frontend/components/dashboard/Web2DashboardView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/components/dashboard/Web2DashboardView.tsx b/frontend/components/dashboard/Web2DashboardView.tsx index 84380cf..cc30312 100644 --- a/frontend/components/dashboard/Web2DashboardView.tsx +++ b/frontend/components/dashboard/Web2DashboardView.tsx @@ -57,6 +57,7 @@ export default function Web2DashboardView({ userEmail = "user@example.com", user const [certificates, setCertificates] = useState([]); const [isLoading, setIsLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(""); + const [, setCurrentPage] = useState(1); const [selectedInspectItem, setSelectedInspectItem] = useState<{ title: string; data: Record } | null>(null); useEffect(() => { From 47d9f59716a4260500521e234e31239146fd99e4 Mon Sep 17 00:00:00 2001 From: macbook Date: Fri, 31 Jul 2026 12:48:34 +0100 Subject: [PATCH 05/10] frontend build fix --- .../verification/components/steps/UploadReview.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/features/verification/components/steps/UploadReview.tsx b/frontend/features/verification/components/steps/UploadReview.tsx index f2f75cf..bd0bdc9 100644 --- a/frontend/features/verification/components/steps/UploadReview.tsx +++ b/frontend/features/verification/components/steps/UploadReview.tsx @@ -105,7 +105,8 @@ export default function UploadReview({ ? 'Restricted access through managed decryption keys.' : 'Readable directly from the public registry for open verification.'; const contentHashValid = isValidSHA256(content?.contentHash ?? ''); - const hasManifest = content?.manifest !== null && content?.manifest !== undefined; + const manifest = content?.manifest ?? null; + const hasManifest = manifest !== null; const canSubmit = contentHashValid && confirmed && walletConnected && !isSubmitting && !submitted; @@ -204,8 +205,8 @@ export default function UploadReview({ } title="Manifest" stepIndex={1} onNavigate={onNavigate} /> {hasManifest && content?.manifestHash ? (
- - + +
) : ( From 5bf8b60025c421df500f54fcc21f1587764b6386 Mon Sep 17 00:00:00 2001 From: macbook Date: Fri, 31 Jul 2026 13:40:52 +0100 Subject: [PATCH 06/10] Fix frontend lint and build issues --- frontend/features/verification/schemas/manifest-schema.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/features/verification/schemas/manifest-schema.ts b/frontend/features/verification/schemas/manifest-schema.ts index d6f8291..110c3d8 100644 --- a/frontend/features/verification/schemas/manifest-schema.ts +++ b/frontend/features/verification/schemas/manifest-schema.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; export const manifestSchema = z.object({ - contentHash: z.string({ required_error: "contentHash is required" }).min(1, "contentHash is required"), - creator: z.string({ required_error: "creator is required" }).min(1, "creator is required").regex(/^G[A-Z2-7]{55}$/, "Invalid Stellar public key"), - timestamp: z.string({ required_error: "timestamp is required" }).datetime({ message: "Invalid timestamp format, must be ISO 8601" }), + contentHash: z.string().min(1, "contentHash is required"), + creator: z.string().min(1, "creator is required").regex(/^G[A-Z2-7]{55}$/, "Invalid Stellar public key"), + timestamp: z.string().min(1, "timestamp is required").datetime({ message: "Invalid timestamp format, must be ISO 8601" }), metadata: z.record(z.string(), z.any()).optional(), }).passthrough(); From d0e35d3d1a6a1ba9d4ee28183e23ef8bbe7a6cab Mon Sep 17 00:00:00 2001 From: macbook Date: Fri, 31 Jul 2026 16:13:59 +0100 Subject: [PATCH 07/10] build fix --- frontend/services/verificationService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/services/verificationService.ts b/frontend/services/verificationService.ts index cdbe74c..64dc2f4 100644 --- a/frontend/services/verificationService.ts +++ b/frontend/services/verificationService.ts @@ -146,8 +146,9 @@ export interface VerificationRequest { certificateId?: string; } -export const getVerificationRequests = async (): Promise => { +export const getVerificationRequests = async (publicKey: string): Promise => { try { + void publicKey; return getMockRequests(); } catch (error) { console.error('Error fetching verification requests:', error); From a1bc3178ccd64c4d03473d7814e11b81edcb0902 Mon Sep 17 00:00:00 2001 From: macbook Date: Fri, 31 Jul 2026 16:28:35 +0100 Subject: [PATCH 08/10] frontend build verification fix --- frontend/app/search/page.tsx | 20 -------- .../__tests__/manifest-schema.test.ts | 4 +- .../components/steps/MediaUploadStep.tsx | 50 +++++++++++-------- .../steps/__tests__/MediaUploadStep.test.tsx | 1 - 4 files changed, 31 insertions(+), 44 deletions(-) diff --git a/frontend/app/search/page.tsx b/frontend/app/search/page.tsx index b065f08..c4380ad 100644 --- a/frontend/app/search/page.tsx +++ b/frontend/app/search/page.tsx @@ -158,26 +158,6 @@ export default function SearchPage() { const verifiedCount = results.filter((r) => r.status === "verified").length; const totalCount = results.length; - /** - * Maps the generic `SearchResult` type to the `Certificate` type required - * by the `GridView` component. This acts as an adapter. - */ - const gridResults: Certificate[] = results.map((r) => ({ - id: r.id, - title: r.name || "Untitled Certificate", - // Placeholder thumbnail logic. Replace with actual data when available. - thumbnailUrl: - r.type === "Image" - ? `https://picsum.photos/seed/${r.id}/400/300` - : r.type === "Video" - ? `https://picsum.photos/seed/${r.id}/400/300` - : r.type === "Audio" - ? `https://picsum.photos/seed/${r.id}/400/300` - : `https://picsum.photos/seed/${r.id}/400/300`, - issuerName: r.creator, - issueDate: r.mintedAt, - })); - return (
diff --git a/frontend/features/verification/__tests__/manifest-schema.test.ts b/frontend/features/verification/__tests__/manifest-schema.test.ts index c79f89a..dbab2fc 100644 --- a/frontend/features/verification/__tests__/manifest-schema.test.ts +++ b/frontend/features/verification/__tests__/manifest-schema.test.ts @@ -47,7 +47,9 @@ describe('Manifest Schema Validation', () => { describe('Invalid Payloads', () => { it('should fail if contentHash is missing', () => { - const { contentHash: _contentHash, ...invalidPayload } = validPayload; + const invalidPayload = Object.fromEntries( + Object.entries(validPayload).filter(([key]) => key !== 'contentHash'), + ); const result = validateManifest(invalidPayload); expect(result.success).toBe(false); if (!result.success) { diff --git a/frontend/features/verification/components/steps/MediaUploadStep.tsx b/frontend/features/verification/components/steps/MediaUploadStep.tsx index d61b560..4b8b0ad 100644 --- a/frontend/features/verification/components/steps/MediaUploadStep.tsx +++ b/frontend/features/verification/components/steps/MediaUploadStep.tsx @@ -1,7 +1,8 @@ 'use client'; import React, { useCallback, useState, useRef, type DragEvent, type ChangeEvent } from 'react'; -import { Upload, FileImage, FileVideo, FileText, X, AlertCircle, CheckCircle2, Loader2, Eye, EyeOff } from 'lucide-react'; +import Image from 'next/image'; +import { Upload, FileImage, FileVideo, FileText, X, AlertCircle, CheckCircle2, Loader2, Eye } from 'lucide-react'; // ── Types ────────────────────────────────────────────────── export interface MediaFile { @@ -99,6 +100,27 @@ export default function MediaUploadStep({ [externalFiles, onFilesChange], ); + const simulateUpload = useCallback( + async (fileId: string) => { + const updateProgress = (progress: number) => { + setFiles((prev) => + prev.map((f) => + f.id === fileId + ? { ...f, progress, status: progress >= 100 ? 'done' : 'uploading' } + : f, + ), + ); + }; + + for (let p = 0; p <= 100; p += 20) { + await new Promise((r) => setTimeout(r, 150)); + updateProgress(p); + } + updateProgress(100); + }, + [setFiles], + ); + // ── File Processing ─────────────────────────────────── const processFiles = useCallback( async (fileList: FileList | File[]) => { @@ -152,27 +174,9 @@ export default function MediaUploadStep({ await simulateUpload(entry.id); } }, - [maxSize, multiple, files, setFiles], + [files, maxSize, multiple, setFiles, simulateUpload], ); - const simulateUpload = async (fileId: string) => { - const updateProgress = (progress: number) => { - setFiles((prev) => - prev.map((f) => - f.id === fileId - ? { ...f, progress, status: progress >= 100 ? 'done' : 'uploading' } - : f, - ), - ); - }; - - for (let p = 0; p <= 100; p += 20) { - await new Promise((r) => setTimeout(r, 150)); - updateProgress(p); - } - updateProgress(100); - }; - // ── Drag Handlers ───────────────────────────────────── const handleDragEnter = useCallback((e: DragEvent) => { e.preventDefault(); @@ -358,10 +362,12 @@ export default function MediaUploadStep({ {file.previewUrl ? (
{isImage ? ( - {file.name} ) : isVideo ? (