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');