diff --git a/example/package.json b/example/package.json index 6505f78..a754863 100644 --- a/example/package.json +++ b/example/package.json @@ -18,6 +18,7 @@ "next": "14.0.1", "react": "^18", "react-dom": "^18", + "react-icons": "^4.11.0", "zuauth": "0.2.1" }, "devDependencies": { diff --git a/example/src/components/TicketFieldsToReveal.tsx b/example/src/components/TicketFieldsToReveal.tsx new file mode 100644 index 0000000..0433a20 --- /dev/null +++ b/example/src/components/TicketFieldsToReveal.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { EdDSATicketFieldsToReveal } from "@pcd/zk-eddsa-event-ticket-pcd"; +import Toggle from '@/components/Toggle'; + +interface TicketFieldsToRevealGridProps { + ticketFieldsToReveal: EdDSATicketFieldsToReveal; + onToggleField: (fieldName: keyof EdDSATicketFieldsToReveal) => void; + disabled?: boolean +} + +// Renders a grid of toggles for revealing specific ticket fields only. +const TicketFieldsToRevealGrid: React.FC = ({ ticketFieldsToReveal, onToggleField, disabled }) => { + const toggleKeys = Object.keys(ticketFieldsToReveal) as Array; + + return ( +
+ {toggleKeys.map(fieldName => ( +
+

{fieldName}

+ onToggleField(fieldName)} + disabled={disabled} + /> +
+ ))} +
+ ); +} + +export default TicketFieldsToRevealGrid; diff --git a/example/src/components/TicketRevealedFieldsInfo.tsx b/example/src/components/TicketRevealedFieldsInfo.tsx new file mode 100644 index 0000000..624f911 --- /dev/null +++ b/example/src/components/TicketRevealedFieldsInfo.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { EdDSATicketFieldsToReveal } from "@pcd/zk-eddsa-event-ticket-pcd"; + +interface RevealedFieldsInfoProps { + user: { + [key: string]: boolean | string | number; + }; + revealedFields: EdDSATicketFieldsToReveal; +} + +const RevealedFieldsInfo: React.FC = ({ user, revealedFields }) => { + const renderedFields = Object.entries(revealedFields).map(([fieldName, shouldReveal]) => { + if (shouldReveal) { + const replaced = fieldName.replace('reveal', '').charAt(0).toLowerCase() + fieldName.slice(7) + const fieldValue = user[replaced]; + return ( +
+
{replaced}
+
{fieldValue.toString()}
+
+ ); + } + return null; + }); + + const allNull = renderedFields.filter(field => field !== null).length === 0; + + return ( +
+ {allNull ? ( +
You're logged in without revealing anything :)
+ ) : ( + renderedFields + )} +
+ ); +}; + +export default RevealedFieldsInfo; diff --git a/example/src/components/Toggle.tsx b/example/src/components/Toggle.tsx new file mode 100644 index 0000000..2b6fa00 --- /dev/null +++ b/example/src/components/Toggle.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { FaToggleOff, FaToggleOn } from "react-icons/fa"; + +interface ToggleProps { + checked?: boolean; + onToggle: () => void; + disabled?: boolean; +} + +const Toggle: React.FC = ({ checked, onToggle, disabled }) => { + const isChecked = checked !== undefined ? checked : false; + + const handleClick = () => { + if (!disabled) { + onToggle(); + } + }; + + return ( + + ); +} + +export default Toggle; \ No newline at end of file diff --git a/example/src/pages/index.tsx b/example/src/pages/index.tsx index 07a63e1..b820408 100644 --- a/example/src/pages/index.tsx +++ b/example/src/pages/index.tsx @@ -3,18 +3,85 @@ import Head from "next/head" import Image from "next/image" import { useCallback, useEffect, useState } from "react" import { useZuAuth } from "zuauth" +import { EdDSATicketFieldsToReveal } from "@pcd/zk-eddsa-event-ticket-pcd" +import TicketFieldsToRevealGrid from "@/components/TicketFieldsToReveal" +import Toggle from "@/components/Toggle" +import RevealedFieldsInfo from "@/components/TicketRevealedFieldsInfo" export default function Home() { const { authenticate, pcd } = useZuAuth() const [user, setUser] = useState() + const [developerMode, setDeveloperMode] = useState(false); + const [ticketFieldsToReveal, setTicketFieldsToReveal] = useState({ + revealTicketId: false, + revealEventId: true, + revealProductId: true, + revealTimestampConsumed: false, + revealTimestampSigned: false, + revealAttendeeSemaphoreId: false, + revealIsConsumed: false, + revealIsRevoked: false, + revealTicketCategory: false, + revealAttendeeEmail: true, + revealAttendeeName: false + }); + + const handleToggleField = (fieldName: keyof EdDSATicketFieldsToReveal) => { + setTicketFieldsToReveal((prevState: EdDSATicketFieldsToReveal) => { + const revealedFields = { + ...prevState, + [fieldName]: !prevState[fieldName] + }; + + localStorage.setItem("ticketFieldsToReveal", JSON.stringify(revealedFields)); + + return revealedFields; + }); + }; + + const handleSetDeveloperMode = () => { + setDeveloperMode((value: boolean) => { + localStorage.setItem("developerMode", JSON.stringify(!value)) + + if (!value === true) { + localStorage.setItem("ticketFieldsToReveal", JSON.stringify(ticketFieldsToReveal)); + } else { + setTicketFieldsToReveal({ + revealTicketId: false, + revealEventId: true, + revealProductId: true, + revealTimestampConsumed: false, + revealTimestampSigned: false, + revealAttendeeSemaphoreId: false, + revealIsConsumed: false, + revealIsRevoked: false, + revealTicketCategory: false, + revealAttendeeEmail: true, + revealAttendeeName: false + }) + + localStorage.removeItem("ticketFieldsToReveal") + } + return !value + }) + } // Every time the page loads, an API call is made to check if the // user is logged in and, if they are, to retrieve the current session's user data. useEffect(() => { - ;(async function () { + ; (async function () { const { data } = await axios.get("/api/user") - setUser(data.user) + + const savedFields = localStorage.getItem("ticketFieldsToReveal"); + if (savedFields) { + setTicketFieldsToReveal(JSON.parse(savedFields)); + } + + const developerMode = localStorage.getItem("developerMode") + if (developerMode) { + setDeveloperMode(JSON.parse(developerMode)) + } })() }, []) @@ -23,23 +90,23 @@ export default function Home() { // Note that the nonce is used as a watermark for the PCD. Therefore, // it will be necessary on the server side to verify that the PCD's // watermark matches the session nonce. - const login = useCallback(async () => { + const login = async () => { const { data } = await axios.post("/api/nonce") authenticate( - { + developerMode ? { ...ticketFieldsToReveal } : { revealAttendeeEmail: true, revealEventId: true, revealProductId: true }, data.nonce ) - }, [authenticate]) + } // When the popup is closed and the user successfully // generates the PCD, they can login. useEffect(() => { - ;(async function () { + ; (async function () { if (pcd) { const { data } = await axios.post("/api/login", { pcd }) @@ -49,11 +116,30 @@ export default function Home() { }, [pcd]) // Logging out simply clears the active session. - const logout = useCallback(async () => { + const logout = async () => { + await axios.post("/api/logout") + localStorage.removeItem("ticketFieldsToReveal") + localStorage.removeItem("developerMode") + + setTicketFieldsToReveal({ + revealTicketId: false, + revealEventId: true, + revealProductId: true, + revealTimestampConsumed: false, + revealTimestampSigned: false, + revealAttendeeSemaphoreId: false, + revealIsConsumed: false, + revealIsRevoked: false, + revealTicketCategory: false, + revealAttendeeEmail: true, + revealAttendeeName: false + }) + setDeveloperMode(false) + setUser(false) - }, []) + } return (
@@ -61,14 +147,20 @@ export default function Home() { ZuAuth Example -
+
ZuAuth Icon
-

Login

+

+ ZuAuth Example +

-

+

+ +
+ +

This demo illustrates how the{" "}

- {user &&
User: {user.attendeeEmail}
} + {!user && + <> +
+

Developer Mode

+ +
-
+
+ {developerMode && ( + + )} +
+ + } + + {user &&
+
}
-
+ ) } diff --git a/example/src/styles/globals.css b/example/src/styles/globals.css index e7b3954..62a72cf 100644 --- a/example/src/styles/globals.css +++ b/example/src/styles/globals.css @@ -24,4 +24,4 @@ body { rgb(var(--background-end-rgb)) ) rgb(var(--background-start-rgb)); -} +} \ No newline at end of file diff --git a/example/yarn.lock b/example/yarn.lock index 4379ea6..8e4dc5d 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -4755,6 +4755,15 @@ __metadata: languageName: node linkType: hard +"react-icons@npm:^4.11.0": + version: 4.11.0 + resolution: "react-icons@npm:4.11.0" + peerDependencies: + react: "*" + checksum: 95e837e11ece80cc39ef1beac026d10f96cd7e567afc718e717517beb35b82dd59307a758c10b3a449dc15d6682d6551ecc630b2821d9365819af921fa279a73 + languageName: node + linkType: hard + "react-is@npm:^16.13.1, react-is@npm:^16.7.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" @@ -5828,6 +5837,7 @@ __metadata: postcss: "npm:^8" react: "npm:^18" react-dom: "npm:^18" + react-icons: "npm:^4.11.0" tailwindcss: "npm:^3.3.0" typescript: "npm:^5" zuauth: "npm:0.2.1"