@@ -34,6 +34,7 @@ const VotingModal: React.FC<VotersModalProps> = ({
3434 const [ selectedWeight , setSelectedWeight ] = useState < number > ( 0 ) ;
3535 const [ isTokenVoting , setIsTokenVoting ] = useState ( false ) ;
3636 const [ tokenBalance , setTokenBalance ] = useState < number > ( 0 ) ;
37+ const [ voteReceipt , setVoteReceipt ] = useState < import ( "types/proposal" ) . VoteReceipt | null > ( null ) ;
3738 const isInsufficientVotingPower = maxWeight <= 0 ;
3839
3940 const insufficientPowerMessage = isTokenVoting
@@ -122,18 +123,20 @@ const VotingModal: React.FC<VotersModalProps> = ({
122123 const { voteToProposal } = await import ( "@service/ContractService" ) ;
123124
124125 try {
125- await voteToProposal (
126+ const receipt = await voteToProposal (
126127 projectName ,
127128 proposalId ! ,
128129 selectedOption as VoteType ,
129130 selectedWeight ,
130131 ) ;
132+
133+ setVoteReceipt ( receipt ) ;
131134 onVoteSuccess ?.( ) ;
132135 toast . success (
133136 "Congratulations!" ,
134137 "Your vote was submitted successfully." ,
135138 ) ;
136- onClose ( ) ;
139+ // We do not close the modal here, we show the receipt instead
137140 } catch ( error : any ) {
138141 let errorMessage = "Failed to cast vote" ;
139142
@@ -154,6 +157,93 @@ const VotingModal: React.FC<VotersModalProps> = ({
154157 }
155158 } ;
156159
160+ if ( voteReceipt ) {
161+ return (
162+ < Modal onClose = { onClose } >
163+ < style > { `
164+ @media print {
165+ body * { visibility: hidden; }
166+ #printable-receipt, #printable-receipt * { visibility: visible; }
167+ #printable-receipt { position: absolute; left: 0; top: 0; width: 100%; padding: 20px; }
168+ .no-print { display: none !important; }
169+ }
170+ ` } </ style >
171+ < div id = "printable-receipt" className = "flex flex-col gap-6 w-full max-w-2xl mx-auto" >
172+ < div className = "flex flex-col gap-2 border-b pb-4" >
173+ < h2 className = "text-2xl font-bold text-primary" > Vote Receipt</ h2 >
174+ < p className = "text-sm text-secondary" >
175+ { voteReceipt . isPublicVoting
176+ ? "Your vote was public. You can check the transaction hash on a Stellar explorer to verify it is on-chain."
177+ : "Your vote was anonymous. Keep this receipt safe. You can verify your commitment on-chain using the transaction hash. At the end of the vote, the tally can be checked by verifying all commitments against the encrypted votes." }
178+ </ p >
179+ </ div >
180+
181+ < div className = "flex flex-col gap-4 bg-gray-50 p-4 rounded-lg overflow-x-auto text-sm" >
182+ < div className = "grid grid-cols-1 sm:grid-cols-2 gap-4" >
183+ < div > < span className = "font-semibold" > Project:</ span > { voteReceipt . projectName } </ div >
184+ < div > < span className = "font-semibold" > Proposal ID:</ span > { voteReceipt . proposalId } </ div >
185+ < div > < span className = "font-semibold" > Vote Type:</ span > < span className = "capitalize" > { voteReceipt . voteType } </ span > </ div >
186+ < div > < span className = "font-semibold" > Weight:</ span > { voteReceipt . weight . toLocaleString ( ) } </ div >
187+ < div > < span className = "font-semibold" > Voting Type:</ span > { voteReceipt . isPublicVoting ? "Public" : "Anonymous" } </ div >
188+ </ div >
189+
190+ { voteReceipt . transactionHash && (
191+ < div className = "mt-2" >
192+ < span className = "font-semibold block mb-1" > Transaction Hash:</ span >
193+ < code className = "bg-white px-2 py-1 rounded border break-all text-xs" > { voteReceipt . transactionHash } </ code >
194+ </ div >
195+ ) }
196+
197+ { ! voteReceipt . isPublicVoting && (
198+ < >
199+ < div className = "mt-2" >
200+ < span className = "font-semibold block mb-1" > Public Key:</ span >
201+ < code className = "bg-white px-2 py-1 rounded border break-all text-xs" > { voteReceipt . publicKey } </ code >
202+ </ div >
203+
204+ < div className = "mt-2" >
205+ < span className = "font-semibold block mb-1" > Encrypted Votes (Approve, Reject, Abstain):</ span >
206+ < div className = "flex flex-col gap-1" >
207+ { voteReceipt . votes ?. map ( ( v , i ) => (
208+ < code key = { i } className = "bg-white px-2 py-1 rounded border break-all text-xs" > { v } </ code >
209+ ) ) }
210+ </ div >
211+ </ div >
212+
213+ < div className = "mt-2" >
214+ < span className = "font-semibold block mb-1" > Cryptographic Seeds:</ span >
215+ < div className = "flex flex-col gap-1" >
216+ { voteReceipt . seeds ?. map ( ( s , i ) => (
217+ < code key = { i } className = "bg-white px-2 py-1 rounded border break-all text-xs" > { s } </ code >
218+ ) ) }
219+ </ div >
220+ </ div >
221+
222+ < div className = "mt-2" >
223+ < span className = "font-semibold block mb-1" > Commitments:</ span >
224+ < div className = "flex flex-col gap-1" >
225+ { voteReceipt . commitments ?. map ( ( c , i ) => (
226+ < code key = { i } className = "bg-white px-2 py-1 rounded border break-all text-xs" > { c } </ code >
227+ ) ) }
228+ </ div >
229+ </ div >
230+ </ >
231+ ) }
232+ </ div >
233+
234+ < div className = "flex justify-end gap-3 no-print mt-4" >
235+ < Button type = "secondary" onClick = { onClose } >
236+ Close
237+ </ Button >
238+ < Button onClick = { ( ) => window . print ( ) } >
239+ Print / Save PDF
240+ </ Button >
241+ </ div >
242+ </ div >
243+ </ Modal >
244+ ) ;
245+ }
246+
157247 return (
158248 < Modal onClose = { onClose } >
159249 < div className = "flex flex-col sm:flex-row items-center sm:items-start gap-6 sm:gap-9" >
0 commit comments