From c6b6e4cf05abd008793be909b1088f3a77a56d6d Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Sun, 24 Aug 2025 23:05:58 +0530 Subject: [PATCH 1/3] feat: fix SSE issue on eVoting and public reveal --- platforms/eVoting/src/app/(app)/[id]/page.tsx | 314 ++++++++++++++++-- .../src/components/blind-voting-interface.tsx | 66 +++- .../src/controllers/VoteController.ts | 83 +++++ platforms/evoting-api/src/index.ts | 3 + 4 files changed, 430 insertions(+), 36 deletions(-) diff --git a/platforms/eVoting/src/app/(app)/[id]/page.tsx b/platforms/eVoting/src/app/(app)/[id]/page.tsx index b9b057b4..c66dc6e8 100644 --- a/platforms/eVoting/src/app/(app)/[id]/page.tsx +++ b/platforms/eVoting/src/app/(app)/[id]/page.tsx @@ -150,7 +150,7 @@ export default function Vote({ params }: { params: Promise<{ id: string }> }) { const updateTimeRemaining = () => { const now = new Date().getTime(); - const deadline = new Date(selectedPoll.deadline).getTime(); + const deadline = new Date(selectedPoll.deadline!).getTime(); const difference = deadline - now; if (difference > 0) { @@ -292,10 +292,10 @@ export default function Vote({ params }: { params: Promise<{ id: string }> }) { - {selectedPoll.mode === "public" ? ( + {selectedPoll.visibility === "public" ? ( <> Public @@ -583,17 +583,179 @@ export default function Vote({ params }: { params: Promise<{ id: string }> }) { ) : voteStatus?.hasVoted === true ? ( // Show voting interface for active polls where user has already voted <> - {/* Show that user has voted */} -
-
- + {/* Show that user has voted with detailed vote information for public polls */} + {selectedPoll.visibility === "public" ? ( +
+ {/* Show that user has voted with detailed vote information */} +
+
+ +
+

Your Vote Details

+

+ Your vote has been submitted. Results will be shown when the poll ends. +

+
+
+ + {/* Display vote details based on mode */} + {(() => { + const voteData = voteStatus?.vote?.data; + if (!voteData) return null; + + if (voteData.mode === "normal" && Array.isArray(voteData.data)) { + // Simple vote - show selected options + const selectedOptions = voteData.data.map(index => selectedPoll.options[parseInt(index)]).filter(Boolean); + return ( +
+

Selected Options:

+
+ {selectedOptions.map((option, i) => ( +
+
+ {option} +
+ ))} +
+
+ ); + } else if (voteData.mode === "point" && typeof voteData.data === "object") { + // Point vote - show point distribution + const pointEntries = Object.entries(voteData.data); + return ( +
+

Your Point Distribution:

+
+ {pointEntries.map(([optionIndex, points]) => { + const option = selectedPoll.options[parseInt(optionIndex)]; + return ( +
+ {option} + + {points} points + +
+ ); + })} +
+
+ + Total: {Object.values(voteData.data).reduce((sum: number, points: any) => sum + points, 0)} points + +
+
+ ); + } else if (voteData.mode === "rank" && Array.isArray(voteData.data)) { + // Rank vote - show ranking + const rankData = voteData.data[0]?.points; + if (rankData && typeof rankData === "object") { + const sortedRanks = Object.entries(rankData) + .sort(([,a], [,b]) => (a as number) - (b as number)) + .map(([optionIndex, rank]) => ({ + option: selectedPoll.options[parseInt(optionIndex)], + rank: rank as number + })); + + return ( +
+

Your Ranking:

+
+ {sortedRanks.map((item, i) => ( +
+ {item.option} + + {item.rank === 1 ? '1st' : item.rank === 2 ? '2nd' : item.rank === 3 ? '3rd' : `${item.rank}th`} choice + +
+ ))} +
+
+ ); + } + } + return null; + })()} +
+ + {/* Show voting options with user's choice highlighted */}
-

Vote Submitted

-

You have already voted on this poll

-

Results will be shown when the poll ends

+

+ Voting Options: +

+
+ {selectedPoll.options.map((option, index) => { + const isUserChoice = (() => { + const voteData = voteStatus?.vote?.data; + if (!voteData) return false; + + if (voteData.mode === "normal" && Array.isArray(voteData.data)) { + return voteData.data.includes(index.toString()); + } else if (voteData.mode === "point" && typeof voteData.data === "object") { + return voteData.data[index] > 0; + } else if (voteData.mode === "rank" && Array.isArray(voteData.data)) { + const rankData = voteData.data[0]?.points; + return rankData && rankData[index]; + } + return false; + })(); + + const userChoiceDetails = (() => { + const voteData = voteStatus?.vote?.data; + if (!voteData) return null; + + if (voteData.mode === "normal" && Array.isArray(voteData.data)) { + return voteData.data.includes(index.toString()) ? "← You voted for this option" : null; + } else if (voteData.mode === "point" && typeof voteData.data === "object") { + const points = voteData.data[index]; + return points > 0 ? `← You gave ${points} points` : null; + } else if (voteData.mode === "rank" && Array.isArray(voteData.data)) { + const rankData = voteData.data[0]?.points; + const rank = rankData?.[index]; + return rank ? `← You ranked this ${rank}${rank === 1 ? 'st' : rank === 2 ? 'nd' : rank === 3 ? 'rd' : 'th'}` : null; + } + return null; + })(); + + return ( +
+
+ + {userChoiceDetails && ( +
+ {userChoiceDetails} +
+ )} +
+
+ ); + })} +
-
+ ) : ( + // For private polls, show simple message +
+
+ +
+

Vote Submitted

+

You have already voted on this poll

+

Results will be shown when the poll ends

+
+
+
+ )} ) : (
@@ -611,31 +773,98 @@ export default function Vote({ params }: { params: Promise<{ id: string }> }) { {/* For public polls, show different interface based on voting status */} {hasVoted ? (
- {/* Show that user has voted */} + {/* Show that user has voted with detailed vote information */}
-
+
-

- You voted for:{" "} - { - (() => { - if (voteStatus?.vote?.data?.mode === "normal" && Array.isArray(voteStatus.vote.data.data)) { - const optionIndex = parseInt(voteStatus.vote.data.data[0] || "0"); - return selectedPoll.options[optionIndex] || "Unknown option"; - } - return "Unknown option"; - })() - } -

+

Your Vote Details

Your vote has been submitted. Results will be shown when the poll ends.

+ + {/* Display vote details based on mode */} + {(() => { + const voteData = voteStatus?.vote?.data; + if (!voteData) return null; + + if (voteData.mode === "normal" && Array.isArray(voteData.data)) { + // Simple vote - show selected options + const selectedOptions = voteData.data.map(index => selectedPoll.options[parseInt(index)]).filter(Boolean); + return ( +
+

Selected Options:

+
+ {selectedOptions.map((option, i) => ( +
+
+ {option} +
+ ))} +
+
+ ); + } else if (voteData.mode === "point" && typeof voteData.data === "object") { + // Point vote - show point distribution + const pointEntries = Object.entries(voteData.data); + return ( +
+

Your Point Distribution:

+
+ {pointEntries.map(([optionIndex, points]) => { + const option = selectedPoll.options[parseInt(optionIndex)]; + return ( +
+ {option} + + {points} points + +
+ ); + })} +
+
+ + Total: {Object.values(voteData.data).reduce((sum: number, points: any) => sum + points, 0)} points + +
+
+ ); + } else if (voteData.mode === "rank" && Array.isArray(voteData.data)) { + // Rank vote - show ranking + const rankData = voteData.data[0]?.points; + if (rankData && typeof rankData === "object") { + const sortedRanks = Object.entries(rankData) + .sort(([,a], [,b]) => (a as number) - (b as number)) + .map(([optionIndex, rank]) => ({ + option: selectedPoll.options[parseInt(optionIndex)], + rank: rank as number + })); + + return ( +
+

Your Ranking:

+
+ {sortedRanks.map((item, i) => ( +
+ {item.option} + + {item.rank === 1 ? '1st' : item.rank === 2 ? '2nd' : item.rank === 3 ? '3rd' : `${item.rank}th`} choice + +
+ ))} +
+
+ ); + } + } + return null; + })()}
- {/* Show voting options with user's choice highlighted (grayed out, no results) */} + {/* Show voting options with user's choice highlighted */}

Voting Options: @@ -643,12 +872,37 @@ export default function Vote({ params }: { params: Promise<{ id: string }> }) {
{selectedPoll.options.map((option, index) => { const isUserChoice = (() => { - if (voteStatus?.vote?.data?.mode === "normal" && Array.isArray(voteStatus.vote.data.data)) { - return voteStatus.vote.data.data.includes(index.toString()); + const voteData = voteStatus?.vote?.data; + if (!voteData) return false; + + if (voteData.mode === "normal" && Array.isArray(voteData.data)) { + return voteData.data.includes(index.toString()); + } else if (voteData.mode === "point" && typeof voteData.data === "object") { + return voteData.data[index] > 0; + } else if (voteData.mode === "rank" && Array.isArray(voteData.data)) { + const rankData = voteData.data[0]?.points; + return rankData && rankData[index]; } return false; })(); + const userChoiceDetails = (() => { + const voteData = voteStatus?.vote?.data; + if (!voteData) return null; + + if (voteData.mode === "normal" && Array.isArray(voteData.data)) { + return voteData.data.includes(index.toString()) ? "← You voted for this option" : null; + } else if (voteData.mode === "point" && typeof voteData.data === "object") { + const points = voteData.data[index]; + return points > 0 ? `← You gave ${points} points` : null; + } else if (voteData.mode === "rank" && Array.isArray(voteData.data)) { + const rankData = voteData.data[0]?.points; + const rank = rankData?.[index]; + return rank ? `← You ranked this ${rank}${rank === 1 ? 'st' : rank === 2 ? '2nd' : rank === 3 ? 'rd' : 'th'}` : null; + } + return null; + })(); + return (
}) { }`}> {option} - {isUserChoice && ( + {userChoiceDetails && (
- ← You voted for this option + {userChoiceDetails}
)}
diff --git a/platforms/eVoting/src/components/blind-voting-interface.tsx b/platforms/eVoting/src/components/blind-voting-interface.tsx index 9ef1e28c..502b6553 100644 --- a/platforms/eVoting/src/components/blind-voting-interface.tsx +++ b/platforms/eVoting/src/components/blind-voting-interface.tsx @@ -1,9 +1,9 @@ import React, { useState, useEffect } from 'react'; import { QRCodeSVG } from 'qrcode.react'; -import { Vote, UserX, Shield } from 'lucide-react'; +import { Vote, UserX, Shield, CheckCircle } from 'lucide-react'; import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; -import type { Poll } from "@shared/schema"; +import type { Poll } from "@/lib/pollApi"; interface BlindVotingInterfaceProps { poll: Poll; @@ -17,8 +17,62 @@ export default function BlindVotingInterface({ poll, userId, hasVoted, onVoteSub const [deepLink, setDeepLink] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); + const [voteStatus, setVoteStatus] = useState<{ hasVoted: boolean; vote: any } | null>(null); + const [isConnected, setIsConnected] = useState(false); const { toast } = useToast(); + // SSE connection for real-time vote status updates + useEffect(() => { + if (!poll.id || !userId) return; + + const apiBaseUrl = process.env.NEXT_PUBLIC_EVOTING_BASE_URL || 'http://localhost:7777'; + const eventSource = new EventSource(`${apiBaseUrl}/api/votes/${poll.id}/status/${userId}`); + + eventSource.onopen = () => { + console.log('🔗 Connected to vote status SSE stream'); + setIsConnected(true); + }; + + eventSource.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + console.log('📡 Vote status update:', data); + + if (data.type === 'connected') { + console.log('✅ SSE connection established'); + } else if (data.type === 'vote_status') { + setVoteStatus({ + hasVoted: data.hasVoted, + vote: data.vote + }); + + // If user just voted, trigger the callback + if (data.hasVoted && !hasVoted) { + console.log('🎉 Vote detected via SSE!'); + onVoteSubmitted(); + } + } else if (data.type === 'error') { + console.error('❌ SSE Error:', data.error); + setError(data.error); + } + } catch (error) { + console.error('❌ Error parsing SSE data:', error); + } + }; + + eventSource.onerror = (error) => { + console.error('❌ SSE connection error:', error); + setIsConnected(false); + }; + + // Cleanup on unmount + return () => { + console.log('🔌 Disconnecting from vote status SSE stream'); + eventSource.close(); + setIsConnected(false); + }; + }, [poll.id, userId, hasVoted, onVoteSubmitted]); + const createBlindVotingDeepLink = async () => { try { setIsLoading(true); @@ -71,13 +125,13 @@ export default function BlindVotingInterface({ poll, userId, hasVoted, onVoteSub } }, []); - // Note: SSE subscription removed as it's not needed for the new blind voting system - // The eID wallet handles the voting process locally and submits directly to the API + // Use the real-time vote status from SSE if available, fallback to prop + const currentHasVoted = voteStatus?.hasVoted ?? hasVoted; - if (hasVoted) { + if (currentHasVoted) { return (
- +

Blind Vote Submitted

Your private vote has been submitted successfully

The vote will remain hidden until revealed

diff --git a/platforms/evoting-api/src/controllers/VoteController.ts b/platforms/evoting-api/src/controllers/VoteController.ts index 23810fe0..8120e648 100644 --- a/platforms/evoting-api/src/controllers/VoteController.ts +++ b/platforms/evoting-api/src/controllers/VoteController.ts @@ -143,4 +143,87 @@ export class VoteController { res.status(500).json({ error: error.message }); } } + + // Monitor vote status for private polls via SSE + async monitorVoteStatus(req: Request, res: Response) { + const { pollId, userId } = req.params; + + if (!pollId || !userId) { + return res.status(400).json({ error: "Poll ID and User ID required" }); + } + + // Set SSE headers + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Cache-Control" + }); + + // Send initial connection message + res.write("data: " + JSON.stringify({ + type: "connected", + pollId, + userId, + message: "Connected to vote status stream" + }) + "\n\n"); + + // Initial vote status check + try { + const vote = await this.voteService.getUserVote(pollId, userId); + const hasVoted = !!vote; + + res.write("data: " + JSON.stringify({ + type: "vote_status", + pollId, + userId, + hasVoted, + vote: hasVoted ? vote : null, + timestamp: new Date().toISOString() + }) + "\n\n"); + } catch (error) { + console.error("Error checking initial vote status:", error); + res.write("data: " + JSON.stringify({ + type: "error", + pollId, + userId, + error: "Failed to check vote status", + timestamp: new Date().toISOString() + }) + "\n\n"); + } + + // Set up polling to check vote status every 2 seconds + const pollInterval = setInterval(async () => { + try { + const vote = await this.voteService.getUserVote(pollId, userId); + const hasVoted = !!vote; + + res.write("data: " + JSON.stringify({ + type: "vote_status", + pollId, + userId, + hasVoted, + vote: hasVoted ? vote : null, + timestamp: new Date().toISOString() + }) + "\n\n"); + } catch (error) { + console.error("Error polling vote status:", error); + // Don't send error events continuously, just log them + } + }, 2000); + + // Handle client disconnect + req.on("close", () => { + clearInterval(pollInterval); + res.end(); + }); + + // Handle errors + req.on("error", (error) => { + console.error("SSE Error:", error); + clearInterval(pollInterval); + res.end(); + }); + } } \ No newline at end of file diff --git a/platforms/evoting-api/src/index.ts b/platforms/evoting-api/src/index.ts index 9ae0f529..8abb1baa 100644 --- a/platforms/evoting-api/src/index.ts +++ b/platforms/evoting-api/src/index.ts @@ -221,6 +221,9 @@ app.get("/api/votes/:pollId/tally", voteController.tallyBlindVotes.bind(voteCont // Generic poll route (must come last to avoid conflicts with specific routes) app.get("/api/polls/:id", pollController.getPollById); +// Vote status monitoring via SSE (for private polls) - Public endpoint for real-time updates +app.get("/api/votes/:pollId/status/:userId", voteController.monitorVoteStatus.bind(voteController)); // Monitor vote status via SSE + // Start server app.listen(port, () => { console.log(`Server running on port ${port}`); From 4e5cd8dd3eae2fa528f1c2df7d67662d993e208c Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Mon, 25 Aug 2025 00:25:36 +0530 Subject: [PATCH 2/3] feat: reveal blind vote --- .../src/lib/ui/Drawer/Drawer.svelte | 92 ++--- .../src/routes/(app)/scan-qr/+page.svelte | 350 ++++++++++++++++-- platforms/eVoting/src/app/(app)/[id]/page.tsx | 30 +- .../src/components/blind-voting-interface.tsx | 62 +++- .../src/controllers/VoteController.ts | 2 + .../evoting-api/src/services/VoteService.ts | 2 + 6 files changed, 439 insertions(+), 99 deletions(-) diff --git a/infrastructure/eid-wallet/src/lib/ui/Drawer/Drawer.svelte b/infrastructure/eid-wallet/src/lib/ui/Drawer/Drawer.svelte index 1e7f4cad..0042d37b 100644 --- a/infrastructure/eid-wallet/src/lib/ui/Drawer/Drawer.svelte +++ b/infrastructure/eid-wallet/src/lib/ui/Drawer/Drawer.svelte @@ -1,56 +1,57 @@
{ })} onswipe={() => handleSwipe?.(isPaneOpen)} bind:this={drawerElem} - use:clickOutside={handleClickOutside} class={cn(restProps.class)} >
diff --git a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte index 6691621e..a7e18281 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/+page.svelte @@ -60,6 +60,14 @@ let isSubmittingBlindVote = $state(false); // Add loading state let blindVoteSuccess = $state(false); // Add success state + // Reveal vote specific state + let isRevealRequest = $state(false); + let revealPollId = $state(null); + let revealError = $state(null); + let isRevealingVote = $state(false); + let revealSuccess = $state(false); + let revealedVoteData = $state(null); + // Debug logging for selectedBlindVoteOption changes $effect(() => { console.log( @@ -95,6 +103,8 @@ // Check if this is a signing request if (res.content.startsWith("w3ds://sign")) { handleSigningRequest(res.content); + } else if (res.content.startsWith("w3ds://reveal")) { + handleRevealRequest(res.content); } else if (res.content.includes("/blind-vote")) { // This is a blind voting request via HTTP URL // Parse the URL and extract the data @@ -376,6 +386,43 @@ } } + function handleRevealRequest(content: string) { + try { + // Parse w3ds://reveal URI scheme + // Format: w3ds://reveal?pollId= + + // Handle w3ds:// scheme by converting to a parseable format + let parseableContent = content; + if (content.startsWith("w3ds://")) { + parseableContent = content.replace( + "w3ds://", + "https://dummy.com/", + ); + } + + const url = new URL(parseableContent); + const pollId = url.searchParams.get("pollId"); + + console.log("🔍 Parsed w3ds://reveal URI:", { + pollId: pollId, + }); + + if (!pollId) { + console.error("Invalid reveal request parameters:", { + pollId, + }); + return; + } + + revealPollId = pollId; + isRevealRequest = true; + // Don't open the code scanned drawer - we want the reveal drawer + // codeScannedDrawerOpen = true; + } catch (error) { + console.error("Error parsing reveal request:", error); + } + } + async function handleSignVote() { if (!signingData || !signingSessionId) return; @@ -744,7 +791,11 @@ // Convert BigInt values to strings for JSON serialization const localVoteData = { pollId: signingData.pollId, + voterId: voterId, // Store voterId for ownership verification optionId: `option_${selectedBlindVoteOption}`, // Use the correct option ID format + chosenOption: selectedBlindVoteOption, // Store the actual chosen option number + optionText: + signingData.pollDetails.options[selectedBlindVoteOption], // Store the actual option text commitments: commitments, anchors: anchors, timestamp: new Date().toISOString(), @@ -1038,6 +1089,170 @@ onDestroy(async () => { await cancelScan(); }); + + async function handleRevealVote() { + if (!revealPollId) return; + + try { + isRevealingVote = true; + revealError = null; + + // Get the vault for identification + const vault = await globalState.vaultController.vault; + if (!vault) { + throw new Error("No vault available for revealing vote"); + } + + // Get the locally stored blind vote data for this poll + const storedVoteKey = `blindVote_${revealPollId}`; + console.log( + "🔍 Debug: Looking for localStorage key:", + storedVoteKey, + ); + + const storedVoteData = localStorage.getItem(storedVoteKey); + console.log("🔍 Debug: Raw storedVoteData:", storedVoteData); + console.log( + "🔍 Debug: storedVoteData type:", + typeof storedVoteData, + ); + console.log( + "🔍 Debug: storedVoteData length:", + storedVoteData?.length, + ); + + if (!storedVoteData) { + throw new Error( + "No blind vote found for this poll. Make sure you submitted a blind vote first.", + ); + } + + try { + console.log("🔍 Debug: Attempting to parse JSON..."); + const parsedVoteData = JSON.parse(storedVoteData); + console.log( + "🔍 Debug: Successfully parsed vote data:", + parsedVoteData, + ); + console.log( + "🔍 Debug: parsedVoteData type:", + typeof parsedVoteData, + ); + console.log( + "🔍 Debug: parsedVoteData keys:", + Object.keys(parsedVoteData), + ); + + // Check if this is the user's own vote + console.log( + "🔍 Debug: Comparing voterId:", + parsedVoteData.voterId, + "with vault.ename:", + vault.ename, + ); + + // Strip @ prefix from both values for comparison + const storedVoterId = + parsedVoteData.voterId?.replace(/^@/, "") || ""; + const currentVoterId = vault.ename?.replace(/^@/, "") || ""; + + console.log( + "🔍 Debug: After stripping @ - storedVoterId:", + storedVoterId, + "currentVoterId:", + currentVoterId, + ); + + if (storedVoterId !== currentVoterId) { + throw new Error("This blind vote does not belong to you."); + } + + // Get the chosen option from the stored data + console.log( + "🔍 Debug: Looking for chosen option in:", + parsedVoteData, + ); + console.log( + "🔍 Debug: parsedVoteData.optionText:", + parsedVoteData.optionText, + ); + console.log( + "🔍 Debug: parsedVoteData.chosenOption:", + parsedVoteData.chosenOption, + ); + console.log( + "🔍 Debug: parsedVoteData.optionId:", + parsedVoteData.optionId, + ); + + const chosenOption = + parsedVoteData.optionText || + `Option ${parsedVoteData.chosenOption + 1}` || + "Unknown option"; + + console.log("🔍 Debug: Final chosenOption:", chosenOption); + + revealedVoteData = { + chosenOption: chosenOption, + pollId: revealPollId, + voterId: vault.ename, + }; + + console.log("✅ Vote revealed successfully from local storage"); + console.log("🔍 Debug: Setting revealSuccess to true"); + console.log( + "🔍 Debug: Final revealedVoteData:", + revealedVoteData, + ); + revealSuccess = true; + + // Don't close the drawer - let it show the revealed vote + // The drawer content will change to show the success state + } catch (parseError: any) { + console.error("❌ JSON Parse Error Details:", parseError); + console.error("❌ Parse Error Message:", parseError.message); + console.error("❌ Parse Error Stack:", parseError.stack); + console.error( + "❌ Raw data that failed to parse:", + storedVoteData, + ); + + // Try to identify what went wrong + if (storedVoteData && typeof storedVoteData === "string") { + try { + // Try to find where the JSON breaks + const firstBrace = storedVoteData.indexOf("{"); + const lastBrace = storedVoteData.lastIndexOf("}"); + console.log("🔍 Debug: First brace at:", firstBrace); + console.log("🔍 Debug: Last brace at:", lastBrace); + if (firstBrace !== -1 && lastBrace !== -1) { + console.log( + "🔍 Debug: Substring to check:", + storedVoteData.substring( + firstBrace, + lastBrace + 1, + ), + ); + } + } catch (debugError) { + console.error( + "❌ Debug parsing also failed:", + debugError, + ); + } + } + + throw new Error( + "Failed to parse stored vote data. The vote may be corrupted.", + ); + } + } catch (error: any) { + console.error("❌ Error revealing vote:", error); + revealError = error.message || "Failed to reveal vote"; + } finally { + isRevealingVote = false; + } + } @@ -1398,6 +1613,103 @@
+ + + {#if revealSuccess && revealedVoteData} +
+

+ You voted for: {revealedVoteData.chosenOption} +

+

+ Poll ID: {revealedVoteData.pollId} +

+
+ +
+ { + // Go back in history instead of potentially causing white screen + window.history.back(); + }} + > + Okay + +
+ {:else} + +
+
+
+ +
+ +

Reveal Your Blind Vote

+

+ You're about to reveal your blind vote for poll: {revealPollId} +

+ +
+

+ Note: Revealing your vote will show your choice + locally in this wallet. This action cannot be undone. +

+
+ + {#if revealError} +
+

+ {revealError} +

+
+ {/if} + +
+ { + // Go back in history instead of potentially causing white screen + window.history.back(); + }} + > + Cancel + + + {#if isRevealingVote} + Revealing... + {:else} + Reveal Vote + {/if} + +
+ {/if} +
+ {#if signingSuccess}

{isBlindVotingRequest - ? "Your blind vote has been submitted and is now hidden from the platform." + ? "Your blind vote has been submitted and is now completely hidden using cryptographic commitments." : signingData?.pollId ? "Your vote has been signed and submitted to the voting system." : "Your message has been signed and submitted successfully."} @@ -1451,39 +1763,3 @@

{/if} - - -{#if blindVoteSuccess} -
-
-
- -
-

- Blind Vote Submitted Successfully! -

-

- Your blind vote has been submitted and is now hidden from the - platform. -

- { - blindVoteSuccess = false; - }} - class="w-full" - > - Continue - -
-
-{/if} diff --git a/platforms/eVoting/src/app/(app)/[id]/page.tsx b/platforms/eVoting/src/app/(app)/[id]/page.tsx index c66dc6e8..5b476f6d 100644 --- a/platforms/eVoting/src/app/(app)/[id]/page.tsx +++ b/platforms/eVoting/src/app/(app)/[id]/page.tsx @@ -208,6 +208,11 @@ export default function Vote({ params }: { params: Promise<{ id: string }> }) { ]); setVoteStatus(voteStatusData); setResultsData(resultsData); + + // Update hasVoted state based on the fetched vote status + if (voteStatusData && voteStatusData.hasVoted) { + setHasVoted(true); + } } catch (error) { console.error("Failed to fetch vote data:", error); } @@ -217,6 +222,13 @@ export default function Vote({ params }: { params: Promise<{ id: string }> }) { fetchVoteData(); }, [pollId, user?.id]); + // Sync hasVoted state with voteStatus when it changes + useEffect(() => { + if (voteStatus) { + setHasVoted(voteStatus.hasVoted); + } + }, [voteStatus]); + if (isLoading) { @@ -744,17 +756,13 @@ export default function Vote({ params }: { params: Promise<{ id: string }> }) {
) : ( - // For private polls, show simple message -
-
- -
-

Vote Submitted

-

You have already voted on this poll

-

Results will be shown when the poll ends

-
-
-
+ // For private polls, show BlindVotingInterface (which handles both voting and reveal) + )} ) : ( diff --git a/platforms/eVoting/src/components/blind-voting-interface.tsx b/platforms/eVoting/src/components/blind-voting-interface.tsx index 502b6553..0f3e1814 100644 --- a/platforms/eVoting/src/components/blind-voting-interface.tsx +++ b/platforms/eVoting/src/components/blind-voting-interface.tsx @@ -128,13 +128,65 @@ export default function BlindVotingInterface({ poll, userId, hasVoted, onVoteSub // Use the real-time vote status from SSE if available, fallback to prop const currentHasVoted = voteStatus?.hasVoted ?? hasVoted; + console.log('🔍 BlindVotingInterface Debug:', { + hasVoted: hasVoted, + voteStatus: voteStatus, + currentHasVoted: currentHasVoted, + pollId: poll.id + }); + if (currentHasVoted) { return ( -
- -

Blind Vote Submitted

-

Your private vote has been submitted successfully

-

The vote will remain hidden until revealed

+
+
+ +

Blind Vote Submitted

+

Your private vote has been submitted successfully

+

The vote will remain hidden until revealed

+
+ + {/* Reveal Vote Section */} +
+
+ +

Reveal Your Vote

+

+ Use this QR code to reveal your vote choice when you're ready +

+
+ + {/* Reveal QR Code */} +
+
+ +
+ +
+

+ Scan this QR code with your eID wallet to reveal your vote +

+

+ Poll ID: {poll.id} +

+
+
+ + {/* Instructions */} +
+
How to Reveal:
+
    +
  1. Scan the reveal QR code with your eID wallet
  2. +
  3. Confirm your identity in the wallet
  4. +
  5. Your vote choice will be revealed locally
  6. +
  7. Your vote remains completely private and anonymous
  8. +
+
+
); } diff --git a/platforms/evoting-api/src/controllers/VoteController.ts b/platforms/evoting-api/src/controllers/VoteController.ts index 8120e648..1ab26c8b 100644 --- a/platforms/evoting-api/src/controllers/VoteController.ts +++ b/platforms/evoting-api/src/controllers/VoteController.ts @@ -144,6 +144,8 @@ export class VoteController { } } + + // Monitor vote status for private polls via SSE async monitorVoteStatus(req: Request, res: Response) { const { pollId, userId } = req.params; diff --git a/platforms/evoting-api/src/services/VoteService.ts b/platforms/evoting-api/src/services/VoteService.ts index 2e97069d..ae68262d 100644 --- a/platforms/evoting-api/src/services/VoteService.ts +++ b/platforms/evoting-api/src/services/VoteService.ts @@ -611,6 +611,8 @@ export class VoteService { throw error; } } + + } export default new VoteService(); \ No newline at end of file From e3b2c53cf8cbc711aab91bfad9c1f170ff333260 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Mon, 25 Aug 2025 00:25:53 +0530 Subject: [PATCH 3/3] chore: bump version --- infrastructure/eid-wallet/package.json | 2 +- .../eid-wallet/src/routes/(app)/settings/+page.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/infrastructure/eid-wallet/package.json b/infrastructure/eid-wallet/package.json index 5f32b9b0..a2b3e07e 100644 --- a/infrastructure/eid-wallet/package.json +++ b/infrastructure/eid-wallet/package.json @@ -1,6 +1,6 @@ { "name": "eid-wallet", - "version": "0.1.0", + "version": "0.2.1", "description": "", "type": "module", "scripts": { diff --git a/infrastructure/eid-wallet/src/routes/(app)/settings/+page.svelte b/infrastructure/eid-wallet/src/routes/(app)/settings/+page.svelte index 3873a605..188ad556 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/settings/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/settings/+page.svelte @@ -126,7 +126,7 @@ on:click={handleVersionTap} disabled={isRetrying} > - Version v0.2.0.2 + Version v0.2.1.0 {#if retryMessage}