Skip to content

Commit

Permalink
Merge pull request #2 from 0xjei/feat/dev-mode
Browse files Browse the repository at this point in the history
Implement developer mode for custom ticket fields to reveal choices
  • Loading branch information
cedoor authored Nov 8, 2023
2 parents 7192ad6 + 209ae25 commit c7c052a
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 38 deletions.
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"next": "14.0.1",
"react": "^18",
"react-dom": "^18",
"react-icons": "^4.11.0",
"zuauth": "0.2.1"
},
"devDependencies": {
Expand Down
32 changes: 32 additions & 0 deletions example/src/components/DeveloperPanel.tsx
Original file line number Diff line number Diff line change
@@ -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<DeveloperPanelProps> = ({ fieldsToReveal, onToggleField, disabled = false }) => {
const toggleKeys = Object.keys(fieldsToReveal) as Array<keyof EdDSATicketFieldsToReveal>;

return (
<div className="grid grid-cols-4 gap-4">
{toggleKeys.map(fieldName => (
<div key={fieldName} className="flex flex-col items-center">
<p className="text-center">{fieldName}</p>
<Toggle
checked={fieldsToReveal[fieldName]}
onToggle={() => onToggleField(fieldName)}
disabled={disabled}
/>
</div>
))}
</div>
);
}

export default DeveloperPanel;
42 changes: 42 additions & 0 deletions example/src/components/DisplayRevealedFields.tsx
Original file line number Diff line number Diff line change
@@ -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<DisplayRevealedFieldsProps> = ({ 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 (
<div key={fieldName} className="my-2 text-center">
<div className="font-bold">{replaced}</div>
<div className="ml-4">{fieldValue.toString()}</div>
</div>
);
}
return null;
});

return (
<div>
{renderedFields.filter(field => field !== null).length === 0 ? (
<div className="text-center">You&apos;re in, anon! No ticket info has been revealed 🕶</div>
) : (
renderedFields
)}
</div>
);
};

export default DisplayRevealedFields;
28 changes: 28 additions & 0 deletions example/src/components/Toggle.tsx
Original file line number Diff line number Diff line change
@@ -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<ToggleProps> = ({ checked = false, onToggle, disabled }) => {
const handleClick = () => {
if (!disabled) {
onToggle();
}
};

return (
<button
className={`toggle-icon ${checked ? 'active' : ''}`}
onClick={handleClick}
disabled={disabled}
>
{checked ? <FaToggleOn size={30} className="text-blue-800"/> : <FaToggleOff size={30} />}
</button>
);
}

export default Toggle;
149 changes: 112 additions & 37 deletions example/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,74 +1,131 @@
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<any>()
const [developerMode, setDeveloperMode] = useState(false);
const [ticketFieldsToReveal, setTicketFieldsToReveal] = useState<EdDSATicketFieldsToReveal>(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 (
<main className="flex min-h-screen flex-col items-center justify-center p-12 pb-32">
<Head>
<title>ZuAuth Example</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
</Head>
<div className="max-w-xl w-full">
<div className="max-w-4xl w-full mx-auto">
<div className="flex justify-center">
<Image width="150" height="150" alt="ZuAuth Icon" src="/light-icon.png" />
</div>

<h1 className="my-8 text-2xl font-semibold text-center">Login</h1>
<h1 className="my-4 text-2xl font-semibold text-center">
ZuAuth Example
</h1>

<p className="text-justify">
<p className="my-8 text-justify">
This demo illustrates how the{" "}
<a
className="text-blue-600 visited:text-purple-600"
Expand All @@ -89,7 +146,8 @@ export default function Home() {
>
IronSession
</a>
. 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{" "}
<a
className="text-blue-600 visited:text-purple-600"
href="https://github.com/cedoor/zuauth#readme"
Expand All @@ -109,14 +167,31 @@ export default function Home() {
</button>
</div>

{user && <div className="text-center">User: {user.attendeeEmail}</div>}
{!user &&
<>
<div className="my-8 text-center flex flex-col items-center">
<p className="mt-2 text-center">Developer Mode</p>
<Toggle
checked={developerMode}
onToggle={handleSetDeveloperMode}
/>
</div>

<div className="mt-10 text-center">
<a href="https://github.com/cedoor/zuauth" className="underline" target="_blank">
Github
</a>
</div>
<div style={{ height: "300px" }}>
{developerMode && (
<DeveloperPanel
fieldsToReveal={ticketFieldsToReveal}
onToggleField={handleToggleField}
disabled={!!user}
/>
)}
</div>
</>
}

{user && <div className="my-8 text-center">
<DisplayRevealedFields user={user} revealedFields={ticketFieldsToReveal} /> </div>}
</div>
</main>
</main >
)
}
}
2 changes: 1 addition & 1 deletion example/src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ body {
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
}
10 changes: 10 additions & 0 deletions example/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit c7c052a

Please sign in to comment.