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/DeveloperPanel.tsx b/example/src/components/DeveloperPanel.tsx new file mode 100644 index 0000000..746d121 --- /dev/null +++ b/example/src/components/DeveloperPanel.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { EdDSATicketFieldsToReveal } from "@pcd/zk-eddsa-event-ticket-pcd"; +import Toggle from '@/components/Toggle'; + +interface DeveloperPanelProps { + fieldsToReveal: EdDSATicketFieldsToReveal; + onToggleField: (fieldName: keyof EdDSATicketFieldsToReveal) => void; + disabled?: boolean; +} + +// Display a set of toggles associated with ticket fields. When a toggle is activated, +// the ticket proof will reveal the corresponding ticket field. +const DeveloperPanel: React.FC = ({ fieldsToReveal, onToggleField, disabled = false }) => { + const toggleKeys = Object.keys(fieldsToReveal) as Array; + + return ( +
+ {toggleKeys.map(fieldName => ( +
+

{fieldName}

+ onToggleField(fieldName)} + disabled={disabled} + /> +
+ ))} +
+ ); +} + +export default DeveloperPanel; diff --git a/example/src/components/DisplayRevealedFields.tsx b/example/src/components/DisplayRevealedFields.tsx new file mode 100644 index 0000000..f8f7b97 --- /dev/null +++ b/example/src/components/DisplayRevealedFields.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { EdDSATicketFieldsToReveal } from "@pcd/zk-eddsa-event-ticket-pcd"; + +interface DisplayRevealedFieldsProps { + user: { + [key: string]: boolean | string | number; + }; + revealedFields: EdDSATicketFieldsToReveal; +} + + +// Display the field name and corresponent value for each one that were revealed. +const DisplayRevealedFields: React.FC = ({ user, revealedFields }) => { + const renderedFields = Object.entries(revealedFields).map(([fieldName, shouldReveal]) => { + if (shouldReveal) { + // Remove the 'reveal' substring and lower the subsequent capitalized letter. + // eg., from 'revealTicketId' to 'ticketId'. + const replaced = fieldName.replace('reveal', '').charAt(0).toLowerCase() + fieldName.slice(7) + + const fieldValue = user[replaced]; + return ( +
+
{replaced}
+
{fieldValue.toString()}
+
+ ); + } + return null; + }); + + return ( +
+ {renderedFields.filter(field => field !== null).length === 0 ? ( +
You're in, anon! No ticket info has been revealed 🕶
+ ) : ( + renderedFields + )} +
+ ); +}; + +export default DisplayRevealedFields; diff --git a/example/src/components/Toggle.tsx b/example/src/components/Toggle.tsx new file mode 100644 index 0000000..052ce72 --- /dev/null +++ b/example/src/components/Toggle.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { FaToggleOff, FaToggleOn } from "react-icons/fa"; + +interface ToggleProps { + checked?: boolean; + onToggle: () => void; + disabled?: boolean; +} + +const Toggle: React.FC = ({ checked = false, onToggle, disabled }) => { + const handleClick = () => { + if (!disabled) { + onToggle(); + } + }; + + return ( + + ); +} + +export default Toggle; diff --git a/example/src/pages/index.tsx b/example/src/pages/index.tsx index 07a63e1..add6260 100644 --- a/example/src/pages/index.tsx +++ b/example/src/pages/index.tsx @@ -1,59 +1,114 @@ import axios from "axios" import Head from "next/head" import Image from "next/image" -import { useCallback, useEffect, useState } from "react" +import { useEffect, useState } from "react" import { useZuAuth } from "zuauth" +import { EdDSATicketFieldsToReveal } from "@pcd/zk-eddsa-event-ticket-pcd" +import Toggle from "@/components/Toggle" +import DisplayRevealedFields from "@/components/DisplayRevealedFields" +import DeveloperPanel from "@/components/DeveloperPanel" + +const defaultSetOfTicketFieldsToReveal: EdDSATicketFieldsToReveal = { + revealTicketId: false, + revealEventId: true, + revealProductId: true, + revealTimestampConsumed: false, + revealTimestampSigned: false, + revealAttendeeSemaphoreId: false, + revealIsConsumed: false, + revealIsRevoked: false, + revealTicketCategory: false, + revealAttendeeEmail: true, + revealAttendeeName: false +} export default function Home() { const { authenticate, pcd } = useZuAuth() const [user, setUser] = useState() + const [developerMode, setDeveloperMode] = useState(false); + const [ticketFieldsToReveal, setTicketFieldsToReveal] = useState(defaultSetOfTicketFieldsToReveal); // 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. + // user is logged in and, if they are, to retrieve the current session's user data + // and local storage data (to guarantee consistency across refreshes). useEffect(() => { - ;(async function () { + ; (async function () { const { data } = await axios.get("/api/user") - setUser(data.user) + + const fields = localStorage.getItem("ticketFieldsToReveal"); + const mode = localStorage.getItem("developerMode") + + if (fields) setTicketFieldsToReveal(JSON.parse(fields)); + if (mode) setDeveloperMode(JSON.parse(mode)) })() }, []) + // When the popup is closed and the user successfully + // generates the PCD, they can login. + useEffect(() => { + ; (async function () { + if (pcd) { + const { data } = await axios.post("/api/login", { pcd }) + setUser(data.user) + } + })() + }, [pcd]) + // Before logging in, the PCD is generated with the nonce from the // session created on the server. // 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( - { - revealAttendeeEmail: true, - revealEventId: true, - revealProductId: true - }, + developerMode ? { ...ticketFieldsToReveal } : { ...defaultSetOfTicketFieldsToReveal }, data.nonce ) - }, [authenticate]) + } - // When the popup is closed and the user successfully - // generates the PCD, they can login. - useEffect(() => { - ;(async function () { - if (pcd) { - const { data } = await axios.post("/api/login", { pcd }) + // Logging out simply clears the active session, local storage and state. + const logout = async () => { + await axios.post("/api/logout") + setUser(false) - setUser(data.user) + localStorage.removeItem("ticketFieldsToReveal") + localStorage.removeItem("developerMode") + + setTicketFieldsToReveal(defaultSetOfTicketFieldsToReveal) + setDeveloperMode(false) + } + + const handleToggleField = (fieldToReveal: keyof EdDSATicketFieldsToReveal) => { + setTicketFieldsToReveal(prevState => { + const fieldsToReveal = { + ...prevState, + [fieldToReveal]: !prevState[fieldToReveal] + }; + + localStorage.setItem("ticketFieldsToReveal", JSON.stringify(fieldsToReveal)); + return fieldsToReveal; + }); + }; + + const handleSetDeveloperMode = () => { + setDeveloperMode(value => { + const newValue = !value + + if (newValue) { + localStorage.setItem("ticketFieldsToReveal", JSON.stringify(ticketFieldsToReveal)); + } else { + setTicketFieldsToReveal(defaultSetOfTicketFieldsToReveal) + localStorage.removeItem("ticketFieldsToReveal") } - })() - }, [pcd]) - // Logging out simply clears the active session. - const logout = useCallback(async () => { - await axios.post("/api/logout") + localStorage.setItem("developerMode", JSON.stringify(newValue)) - setUser(false) - }, []) + return newValue + }) + } return (
@@ -61,14 +116,16 @@ export default function Home() { ZuAuth Example -
+
ZuAuth Icon
-

Login

+

+ ZuAuth Example +

-

+

This demo illustrates how the{" "} IronSession - . Check the{" "} + . You can choose which ticket fields to reveal during the authentication process by enabling the Developer Mode. + We kindly invite you to check the{" "}

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

Developer Mode

+ +
-
+
+ {developerMode && ( + + )} +
+ + } + + {user &&
+
}
-
+ ) -} +} \ No newline at end of file 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"