diff --git a/platforms/eReputation/client/src/components/modals/reference-modal.tsx b/platforms/eReputation/client/src/components/modals/reference-modal.tsx index 4609dae7..0aad32a1 100644 --- a/platforms/eReputation/client/src/components/modals/reference-modal.tsx +++ b/platforms/eReputation/client/src/components/modals/reference-modal.tsx @@ -1,8 +1,10 @@ -import { useState } from "react"; +import { useState, useEffect, useRef } from "react"; import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query"; import { useToast } from "@/hooks/use-toast"; import { isUnauthorizedError } from "@/lib/authUtils"; import { apiClient } from "@/lib/apiClient"; +import { QRCodeSVG } from "qrcode.react"; +import { isMobileDevice, getDeepLinkUrl } from "@/lib/utils/mobile-detection"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; @@ -62,6 +64,10 @@ export default function ReferenceModal({ open, onOpenChange }: ReferenceModalPro const [selectedTarget, setSelectedTarget] = useState(null); const [referenceText, setReferenceText] = useState(""); const [referenceType, setReferenceType] = useState(""); + const [signingSession, setSigningSession] = useState<{ sessionId: string; qrData: string; expiresAt: string } | null>(null); + const [signingStatus, setSigningStatus] = useState<"pending" | "connecting" | "signed" | "expired" | "error" | "security_violation">("pending"); + const [timeRemaining, setTimeRemaining] = useState(900); // 15 minutes in seconds + const [eventSource, setEventSource] = useState(null); const { toast } = useToast(); const queryClient = useQueryClient(); @@ -95,15 +101,23 @@ export default function ReferenceModal({ open, onOpenChange }: ReferenceModalPro const response = await apiClient.post('/api/references', data); return response.data; }, - onSuccess: () => { - toast({ - title: "Reference Submitted", - description: "Your professional reference has been successfully submitted.", - }); - queryClient.invalidateQueries({ queryKey: ["/api/dashboard/stats"] }); - queryClient.invalidateQueries({ queryKey: ["/api/dashboard/activities"] }); - onOpenChange(false); - resetForm(); + onSuccess: (data) => { + // Reference created, now we need to sign it + if (data.signingSession) { + setSigningSession(data.signingSession); + setSigningStatus("pending"); + const expiresAt = new Date(data.signingSession.expiresAt); + const now = new Date(); + const secondsRemaining = Math.floor((expiresAt.getTime() - now.getTime()) / 1000); + setTimeRemaining(Math.max(0, secondsRemaining)); + startSSEConnection(data.signingSession.sessionId); + } else { + // Fallback if no signing session (shouldn't happen) + toast({ + title: "Reference Created", + description: "Your reference has been created. Please sign it to complete.", + }); + } }, onError: (error) => { if (isUnauthorizedError(error)) { @@ -125,12 +139,136 @@ export default function ReferenceModal({ open, onOpenChange }: ReferenceModalPro }, }); + const startSSEConnection = (sessionId: string) => { + // Prevent multiple SSE connections + if (eventSource) { + eventSource.close(); + } + + // Connect to the backend SSE endpoint for signing status + const baseURL = import.meta.env.VITE_EREPUTATION_BASE_URL || "http://localhost:8765"; + const sseUrl = `${baseURL}/api/references/signing/session/${sessionId}/status`; + + const newEventSource = new EventSource(sseUrl); + + newEventSource.onopen = () => { + console.log("SSE connection established for reference signing"); + }; + + newEventSource.onmessage = (e) => { + try { + const data = JSON.parse(e.data); + + if (data.type === "signed" && data.status === "completed") { + setSigningStatus("signed"); + newEventSource.close(); + + toast({ + title: "Reference Signed!", + description: "Your eReference has been successfully signed and submitted.", + }); + + queryClient.invalidateQueries({ queryKey: ["/api/dashboard/stats"] }); + queryClient.invalidateQueries({ queryKey: ["/api/dashboard/activities"] }); + + // Close modal and reset after a short delay + setTimeout(() => { + onOpenChange(false); + resetForm(); + }, 1500); + } else if (data.type === "expired") { + setSigningStatus("expired"); + newEventSource.close(); + toast({ + title: "Session Expired", + description: "The signing session has expired. Please try again.", + variant: "destructive", + }); + } else if (data.type === "security_violation") { + setSigningStatus("security_violation"); + newEventSource.close(); + toast({ + title: "eName Verification Failed", + description: "eName verification failed. Please check your eID.", + variant: "destructive", + }); + } else { + console.log("SSE message:", data); + } + } catch (error) { + console.error("Error parsing SSE data:", error); + } + }; + + newEventSource.onerror = (error) => { + console.error("SSE connection error:", error); + setSigningStatus("error"); + }; + + setEventSource(newEventSource); + }; + + // Countdown timer + useEffect(() => { + if (signingStatus === "pending" && timeRemaining > 0 && signingSession) { + const timer = setInterval(() => { + setTimeRemaining(prev => { + if (prev <= 1) { + setSigningStatus("expired"); + if (eventSource) { + eventSource.close(); + } + return 0; + } + return prev - 1; + }); + }, 1000); + + return () => clearInterval(timer); + } + }, [signingStatus, timeRemaining, signingSession, eventSource]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (eventSource) { + eventSource.close(); + } + }; + }, [eventSource]); + + // Reset signing state when modal closes + useEffect(() => { + if (!open) { + if (eventSource) { + eventSource.close(); + setEventSource(null); + } + setSigningSession(null); + setSigningStatus("pending"); + setTimeRemaining(900); + } + }, [open, eventSource]); + + const formatTime = (seconds: number): string => { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; + }; + const resetForm = () => { setTargetType(""); setSearchQuery(""); setSelectedTarget(null); setReferenceText(""); setReferenceType(""); + setSigningSession(null); + setSigningStatus("pending"); + setTimeRemaining(900); + if (eventSource) { + eventSource.close(); + setEventSource(null); + } }; const handleSearchChange = (value: string) => { @@ -213,7 +351,101 @@ export default function ReferenceModal({ open, onOpenChange }: ReferenceModalPro
-
+ {signingSession ? ( + // Signing Interface +
+
+

Sign Your eReference

+

+ Scan this QR code with your eID Wallet to sign your eReference +

+
+ + {signingSession.qrData && ( + <> + {isMobileDevice() ? ( +
+ + Sign eReference with eID Wallet + +
+ Click the button to open your eID wallet app and sign your eReference +
+
+ ) : ( +
+ +
+ )} + + )} + +
+
+ + + + + Session expires in {formatTime(timeRemaining)} + +
+ + {signingStatus === "signed" && ( +
+ + + + Reference Signed Successfully! +
+ )} + + {signingStatus === "expired" && ( +
+ + + + Session Expired +
+ )} + + {signingStatus === "security_violation" && ( +
+ + + + eName Verification Failed +
+ )} +
+ + {(signingStatus === "expired" || signingStatus === "security_violation" || signingStatus === "error") && ( + + )} +
+ ) : ( + // Reference Form +
{/* Target Selection */}

Select eReference Target

@@ -342,40 +574,62 @@ export default function ReferenceModal({ open, onOpenChange }: ReferenceModalPro {referenceText.length} / 500 characters
-
+
+ )} -
-
+ {!signingSession && ( +
+
+ + +
+
+ )} + + {signingSession && signingStatus !== "signed" && ( +
-
-
+ )} );