From c852aa92f84d0c18b1bd7361163498a542461d45 Mon Sep 17 00:00:00 2001 From: Pawel Zelawski Date: Thu, 10 Apr 2025 10:55:22 +0200 Subject: feat(client): implement react frontend application - Created main entry point `src/client/main.tsx`. - Created main `App` component `src/client/App.tsx`. - Implemented state management (`initial`, `waiting`, `success`, `failed`) using `useState`. - Implemented `handleStart` to call backend `/api/digiid/start` and transition to `waiting` state. - Implemented `useEffect` hook for polling backend `/api/digiid/status/:sessionId` in `waiting` state. - Updates UI state based on polling response (`success`/`failed`). - Handles polling errors and cleanup. - Implemented `handleReset` to return to `initial` state. - Implemented views for each state: - `initial`: Welcome message, Start button. - `waiting`: QR code display, status message, Cancel button. - `success`: Success message, verified address, address type, Start Over button. - `failed`: Failure message, error details, Try Again button. - Added client-side address type helper `getDigiByteAddressType` in `src/client/utils.ts`. - Added basic CSS styling in `src/client/index.css` with light/dark mode support. - Added frontend-related scripts (`dev:frontend`, `dev`, `build`, `lint`, `preview`) to `package.json`. - Fixed linter type issue with `setInterval` return value. --- src/client/App.tsx | 193 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 src/client/App.tsx (limited to 'src/client/App.tsx') diff --git a/src/client/App.tsx b/src/client/App.tsx new file mode 100644 index 0000000..00280cd --- /dev/null +++ b/src/client/App.tsx @@ -0,0 +1,193 @@ +import React, { useState, useEffect } from 'react'; +import { getDigiByteAddressType } from './utils'; // Import the address type helper + +// Define the possible UI states +type UiState = 'initial' | 'waiting' | 'success' | 'failed'; + +// Define the structure for result data (success or failure) +interface ResultData { + address?: string; // Present on success + error?: string; // Present on failure + addressType?: string; // Added later +} + +function App() { + const [uiState, setUiState] = useState('initial'); + const [sessionId, setSessionId] = useState(null); + const [qrCodeDataUrl, setQrCodeDataUrl] = useState(null); + const [resultData, setResultData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); // For general fetch errors + + // Polling interval for status check (in milliseconds) + const POLLING_INTERVAL = 2000; // Check every 2 seconds + + // Effect for polling the status endpoint when in 'waiting' state + useEffect(() => { + // Only poll if we are in the waiting state and have a session ID + if (uiState !== 'waiting' || !sessionId) { + return; // Exit if not applicable + } + + console.log(`Starting status polling for session: ${sessionId}`); + let intervalId: any = null; // Use 'any' to avoid browser/node type conflict for setInterval return type + + const checkStatus = async () => { + if (!sessionId) return; // Should not happen here, but type guard + + console.log(`Checking status for session: ${sessionId}`); + try { + const response = await fetch(`/api/digiid/status/${sessionId}`); + if (!response.ok) { + // Handle specific errors like 404 (session not found/expired) + if (response.status === 404) { + console.warn(`Session ${sessionId} not found or expired during polling.`); + setError('Session expired or could not be found.'); + setUiState('failed'); // Transition to failed state + setResultData({ error: 'Session expired or could not be found.' }); + } else { + const errorData = await response.json().catch(() => ({ message: 'Error fetching status' })); + throw new Error(errorData.message || `Server responded with ${response.status}`); + } + if (intervalId) clearInterval(intervalId); // Stop polling on error + return; + } + + const data: { status: UiState, address?: string, error?: string } = await response.json(); + console.log('Received status data:', data); + + // If status changed from pending, update UI and stop polling + if (data.status === 'success') { + setResultData({ address: data.address }); + setUiState('success'); + if (intervalId) clearInterval(intervalId); + } else if (data.status === 'failed') { + setResultData({ error: data.error || 'Authentication failed.' }); + setUiState('failed'); + if (intervalId) clearInterval(intervalId); + } + // If status is still 'pending', the interval will continue + + } catch (err) { + console.error('Error polling status:', err); + const message = err instanceof Error ? err.message : 'An unknown error occurred during status check'; + setError(`Status polling failed: ${message}`); + // Decide if we should stop polling or transition state on generic fetch error + // For now, let's stop polling and show error, moving to failed state + setResultData({ error: `Status polling failed: ${message}` }); + setUiState('failed'); + if (intervalId) clearInterval(intervalId); + } + }; + + // Start the interval + intervalId = setInterval(checkStatus, POLLING_INTERVAL); + + // Cleanup function to clear the interval when the component unmounts + // or when the dependencies (uiState, sessionId) change + return () => { + if (intervalId) { + console.log(`Stopping status polling for session: ${sessionId}`); + clearInterval(intervalId); + } + }; + }, [uiState, sessionId]); // Dependencies for the effect + + const handleStart = async () => { + setIsLoading(true); + setError(null); + setQrCodeDataUrl(null); // Clear previous QR code if any + setSessionId(null); + setResultData(null); + console.log('Requesting new DigiID session...'); + + try { + const response = await fetch('/api/digiid/start'); + if (!response.ok) { + const errorData = await response.json().catch(() => ({ message: 'Failed to start session. Server responded with status: ' + response.status })); + throw new Error(errorData.message || 'Failed to start session'); + } + const data = await response.json(); + console.log('Received session data:', data); + setSessionId(data.sessionId); + setQrCodeDataUrl(data.qrCodeDataUrl); + setUiState('waiting'); // Move to waiting state + } catch (err) { + console.error('Error starting DigiID session:', err); + const message = err instanceof Error ? err.message : 'An unknown error occurred'; + setError(`Failed to initiate DigiID: ${message}`); + setUiState('initial'); // Stay in initial state on error + } finally { + setIsLoading(false); + } + }; + + const handleReset = () => { + // TODO: Implement reset logic + console.log('Resetting flow...'); + setUiState('initial'); + setSessionId(null); + setQrCodeDataUrl(null); + setResultData(null); + setError(null); + setIsLoading(false); + }; + + return ( +
+

DigiID-TS Demo

+ + {/* --- Initial State --- */} + {uiState === 'initial' && ( +
+

Welcome

+

Click the button below to generate a DigiID login QR code.

+ {/* TODO: Add Icon here */} + {/* DigiID Icon */} + +
+ )} + + {/* --- Waiting State --- */} + {uiState === 'waiting' && qrCodeDataUrl && ( +
+

Scan the QR Code

+

Scan the QR code below using your DigiID compatible mobile wallet.

+ DigiID QR Code +

Waiting for authentication...

+ {/* Optional: Add a cancel button here */} + +
+ )} + + {/* --- Success State --- */} + {uiState === 'success' && resultData?.address && ( +
+

Authentication Successful!

+

Verified Address:

+

{resultData.address}

+

Address Type: {getDigiByteAddressType(resultData.address)}

+ +
+ )} + + {/* --- Failed State --- */} + {uiState === 'failed' && ( +
+

Authentication Failed

+

+ Reason: {resultData?.error || error || 'An unknown error occurred.'} +

+ +
+ )} + + {/* General error display (e.g., for initial start error) */} + {error && uiState === 'initial' &&

Error: {error}

} +
+ ); +} + +export default App; \ No newline at end of file -- cgit v1.2.3