diff --git a/dashboard/src/components/InteractiveWebview.test.tsx b/dashboard/src/components/InteractiveWebview.test.tsx new file mode 100644 index 00000000..9371067a --- /dev/null +++ b/dashboard/src/components/InteractiveWebview.test.tsx @@ -0,0 +1,88 @@ +import { act, render, screen, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { InteractiveWebview } from './InteractiveWebview'; + +describe('InteractiveWebview', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.restoreAllMocks(); + }); + + it('ignores messages from unexpected origins', async () => { + const onComplete = vi.fn(); + render( + + ); + + act(() => { + window.dispatchEvent( + new MessageEvent('message', { + origin: 'https://malicious.example', + data: { transaction: { status: 'completed' } }, + }) + ); + }); + + act(() => { + vi.advanceTimersByTime(2000); + }); + + expect(onComplete).not.toHaveBeenCalled(); + }); + + it('triggers onComplete when status is pending_user_transfer_start', async () => { + const onComplete = vi.fn(); + render( + + ); + + act(() => { + window.dispatchEvent( + new MessageEvent('message', { + origin: 'https://kyc.testanchor.example', + data: { transaction: { status: 'pending_user_transfer_start' } }, + }) + ); + }); + + act(() => { + vi.advanceTimersByTime(2000); + }); + + expect(onComplete).toHaveBeenCalled(); + }); + + it('triggers onComplete when status is completed and data is JSON string', async () => { + const onComplete = vi.fn(); + render( + + ); + + act(() => { + window.dispatchEvent( + new MessageEvent('message', { + origin: 'https://kyc.testanchor.example', + data: JSON.stringify({ transaction: { status: 'completed' } }), + }) + ); + }); + + act(() => { + vi.advanceTimersByTime(2000); + }); + + expect(onComplete).toHaveBeenCalled(); + }); +}); diff --git a/dashboard/src/components/InteractiveWebview.tsx b/dashboard/src/components/InteractiveWebview.tsx index 9808650c..a6e8349f 100644 --- a/dashboard/src/components/InteractiveWebview.tsx +++ b/dashboard/src/components/InteractiveWebview.tsx @@ -35,18 +35,49 @@ export const InteractiveWebview = ({ const [simulatedField, setSimulatedField] = useState(''); useEffect(() => { + let expectedOrigin: string | null = null; + if (interactiveUrl) { + try { + expectedOrigin = new URL(interactiveUrl).origin; + } catch (e) { + // Invalid URL + } + } + const handleMessage = (event: MessageEvent) => { + if (expectedOrigin && event.origin !== expectedOrigin) { + console.warn('Ignoring message from unexpected origin:', event.origin); + return; + } + try { - if (event.data?.type === 'resize' || (typeof event.data === 'string' && event.data.includes('resize'))) { + let data = event.data; + if (typeof data === 'string') { + try { + data = JSON.parse(data); + } catch (e) { + // Not a JSON string + } + } + + if (data?.type === 'resize' || (typeof event.data === 'string' && event.data.includes('resize'))) { console.log('Interactive window resize message received', event.data); } + + if (data?.transaction?.status) { + const status = data.transaction.status; + if (status === 'pending_user_transfer_start' || status === 'completed') { + setWebviewState('approved'); + setTimeout(() => onComplete(), 1200); + } + } } catch (e) { // Ignore parsing errors } }; window.addEventListener('message', handleMessage); return () => window.removeEventListener('message', handleMessage); - }, []); + }, [interactiveUrl, onComplete]); const handleLaunch = () => { setWebviewState('loading');