Skip to content

Commit c7c052a

Browse files
authored
Merge pull request #2 from 0xjei/feat/dev-mode
Implement developer mode for custom ticket fields to reveal choices
2 parents 7192ad6 + 209ae25 commit c7c052a

File tree

7 files changed

+226
-38
lines changed

7 files changed

+226
-38
lines changed

example/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"next": "14.0.1",
1919
"react": "^18",
2020
"react-dom": "^18",
21+
"react-icons": "^4.11.0",
2122
"zuauth": "0.2.1"
2223
},
2324
"devDependencies": {
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React from 'react';
2+
import { EdDSATicketFieldsToReveal } from "@pcd/zk-eddsa-event-ticket-pcd";
3+
import Toggle from '@/components/Toggle';
4+
5+
interface DeveloperPanelProps {
6+
fieldsToReveal: EdDSATicketFieldsToReveal;
7+
onToggleField: (fieldName: keyof EdDSATicketFieldsToReveal) => void;
8+
disabled?: boolean;
9+
}
10+
11+
// Display a set of toggles associated with ticket fields. When a toggle is activated,
12+
// the ticket proof will reveal the corresponding ticket field.
13+
const DeveloperPanel: React.FC<DeveloperPanelProps> = ({ fieldsToReveal, onToggleField, disabled = false }) => {
14+
const toggleKeys = Object.keys(fieldsToReveal) as Array<keyof EdDSATicketFieldsToReveal>;
15+
16+
return (
17+
<div className="grid grid-cols-4 gap-4">
18+
{toggleKeys.map(fieldName => (
19+
<div key={fieldName} className="flex flex-col items-center">
20+
<p className="text-center">{fieldName}</p>
21+
<Toggle
22+
checked={fieldsToReveal[fieldName]}
23+
onToggle={() => onToggleField(fieldName)}
24+
disabled={disabled}
25+
/>
26+
</div>
27+
))}
28+
</div>
29+
);
30+
}
31+
32+
export default DeveloperPanel;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from 'react';
2+
import { EdDSATicketFieldsToReveal } from "@pcd/zk-eddsa-event-ticket-pcd";
3+
4+
interface DisplayRevealedFieldsProps {
5+
user: {
6+
[key: string]: boolean | string | number;
7+
};
8+
revealedFields: EdDSATicketFieldsToReveal;
9+
}
10+
11+
12+
// Display the field name and corresponent value for each one that were revealed.
13+
const DisplayRevealedFields: React.FC<DisplayRevealedFieldsProps> = ({ user, revealedFields }) => {
14+
const renderedFields = Object.entries(revealedFields).map(([fieldName, shouldReveal]) => {
15+
if (shouldReveal) {
16+
// Remove the 'reveal' substring and lower the subsequent capitalized letter.
17+
// eg., from 'revealTicketId' to 'ticketId'.
18+
const replaced = fieldName.replace('reveal', '').charAt(0).toLowerCase() + fieldName.slice(7)
19+
20+
const fieldValue = user[replaced];
21+
return (
22+
<div key={fieldName} className="my-2 text-center">
23+
<div className="font-bold">{replaced}</div>
24+
<div className="ml-4">{fieldValue.toString()}</div>
25+
</div>
26+
);
27+
}
28+
return null;
29+
});
30+
31+
return (
32+
<div>
33+
{renderedFields.filter(field => field !== null).length === 0 ? (
34+
<div className="text-center">You&apos;re in, anon! No ticket info has been revealed 🕶</div>
35+
) : (
36+
renderedFields
37+
)}
38+
</div>
39+
);
40+
};
41+
42+
export default DisplayRevealedFields;

example/src/components/Toggle.tsx

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react';
2+
import { FaToggleOff, FaToggleOn } from "react-icons/fa";
3+
4+
interface ToggleProps {
5+
checked?: boolean;
6+
onToggle: () => void;
7+
disabled?: boolean;
8+
}
9+
10+
const Toggle: React.FC<ToggleProps> = ({ checked = false, onToggle, disabled }) => {
11+
const handleClick = () => {
12+
if (!disabled) {
13+
onToggle();
14+
}
15+
};
16+
17+
return (
18+
<button
19+
className={`toggle-icon ${checked ? 'active' : ''}`}
20+
onClick={handleClick}
21+
disabled={disabled}
22+
>
23+
{checked ? <FaToggleOn size={30} className="text-blue-800"/> : <FaToggleOff size={30} />}
24+
</button>
25+
);
26+
}
27+
28+
export default Toggle;

example/src/pages/index.tsx

+112-37
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,131 @@
11
import axios from "axios"
22
import Head from "next/head"
33
import Image from "next/image"
4-
import { useCallback, useEffect, useState } from "react"
4+
import { useEffect, useState } from "react"
55
import { useZuAuth } from "zuauth"
6+
import { EdDSATicketFieldsToReveal } from "@pcd/zk-eddsa-event-ticket-pcd"
7+
import Toggle from "@/components/Toggle"
8+
import DisplayRevealedFields from "@/components/DisplayRevealedFields"
9+
import DeveloperPanel from "@/components/DeveloperPanel"
10+
11+
const defaultSetOfTicketFieldsToReveal: EdDSATicketFieldsToReveal = {
12+
revealTicketId: false,
13+
revealEventId: true,
14+
revealProductId: true,
15+
revealTimestampConsumed: false,
16+
revealTimestampSigned: false,
17+
revealAttendeeSemaphoreId: false,
18+
revealIsConsumed: false,
19+
revealIsRevoked: false,
20+
revealTicketCategory: false,
21+
revealAttendeeEmail: true,
22+
revealAttendeeName: false
23+
}
624

725
export default function Home() {
826
const { authenticate, pcd } = useZuAuth()
927
const [user, setUser] = useState<any>()
28+
const [developerMode, setDeveloperMode] = useState(false);
29+
const [ticketFieldsToReveal, setTicketFieldsToReveal] = useState<EdDSATicketFieldsToReveal>(defaultSetOfTicketFieldsToReveal);
1030

1131
// Every time the page loads, an API call is made to check if the
12-
// user is logged in and, if they are, to retrieve the current session's user data.
32+
// user is logged in and, if they are, to retrieve the current session's user data
33+
// and local storage data (to guarantee consistency across refreshes).
1334
useEffect(() => {
14-
;(async function () {
35+
; (async function () {
1536
const { data } = await axios.get("/api/user")
16-
1737
setUser(data.user)
38+
39+
const fields = localStorage.getItem("ticketFieldsToReveal");
40+
const mode = localStorage.getItem("developerMode")
41+
42+
if (fields) setTicketFieldsToReveal(JSON.parse(fields));
43+
if (mode) setDeveloperMode(JSON.parse(mode))
1844
})()
1945
}, [])
2046

47+
// When the popup is closed and the user successfully
48+
// generates the PCD, they can login.
49+
useEffect(() => {
50+
; (async function () {
51+
if (pcd) {
52+
const { data } = await axios.post("/api/login", { pcd })
53+
setUser(data.user)
54+
}
55+
})()
56+
}, [pcd])
57+
2158
// Before logging in, the PCD is generated with the nonce from the
2259
// session created on the server.
2360
// Note that the nonce is used as a watermark for the PCD. Therefore,
2461
// it will be necessary on the server side to verify that the PCD's
2562
// watermark matches the session nonce.
26-
const login = useCallback(async () => {
63+
const login = async () => {
2764
const { data } = await axios.post("/api/nonce")
2865

2966
authenticate(
30-
{
31-
revealAttendeeEmail: true,
32-
revealEventId: true,
33-
revealProductId: true
34-
},
67+
developerMode ? { ...ticketFieldsToReveal } : { ...defaultSetOfTicketFieldsToReveal },
3568
data.nonce
3669
)
37-
}, [authenticate])
70+
}
3871

39-
// When the popup is closed and the user successfully
40-
// generates the PCD, they can login.
41-
useEffect(() => {
42-
;(async function () {
43-
if (pcd) {
44-
const { data } = await axios.post("/api/login", { pcd })
72+
// Logging out simply clears the active session, local storage and state.
73+
const logout = async () => {
74+
await axios.post("/api/logout")
75+
setUser(false)
4576

46-
setUser(data.user)
77+
localStorage.removeItem("ticketFieldsToReveal")
78+
localStorage.removeItem("developerMode")
79+
80+
setTicketFieldsToReveal(defaultSetOfTicketFieldsToReveal)
81+
setDeveloperMode(false)
82+
}
83+
84+
const handleToggleField = (fieldToReveal: keyof EdDSATicketFieldsToReveal) => {
85+
setTicketFieldsToReveal(prevState => {
86+
const fieldsToReveal = {
87+
...prevState,
88+
[fieldToReveal]: !prevState[fieldToReveal]
89+
};
90+
91+
localStorage.setItem("ticketFieldsToReveal", JSON.stringify(fieldsToReveal));
92+
return fieldsToReveal;
93+
});
94+
};
95+
96+
const handleSetDeveloperMode = () => {
97+
setDeveloperMode(value => {
98+
const newValue = !value
99+
100+
if (newValue) {
101+
localStorage.setItem("ticketFieldsToReveal", JSON.stringify(ticketFieldsToReveal));
102+
} else {
103+
setTicketFieldsToReveal(defaultSetOfTicketFieldsToReveal)
104+
localStorage.removeItem("ticketFieldsToReveal")
47105
}
48-
})()
49-
}, [pcd])
50106

51-
// Logging out simply clears the active session.
52-
const logout = useCallback(async () => {
53-
await axios.post("/api/logout")
107+
localStorage.setItem("developerMode", JSON.stringify(newValue))
54108

55-
setUser(false)
56-
}, [])
109+
return newValue
110+
})
111+
}
57112

58113
return (
59114
<main className="flex min-h-screen flex-col items-center justify-center p-12 pb-32">
60115
<Head>
61116
<title>ZuAuth Example</title>
62117
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
63118
</Head>
64-
<div className="max-w-xl w-full">
119+
<div className="max-w-4xl w-full mx-auto">
65120
<div className="flex justify-center">
66121
<Image width="150" height="150" alt="ZuAuth Icon" src="/light-icon.png" />
67122
</div>
68123

69-
<h1 className="my-8 text-2xl font-semibold text-center">Login</h1>
124+
<h1 className="my-4 text-2xl font-semibold text-center">
125+
ZuAuth Example
126+
</h1>
70127

71-
<p className="text-justify">
128+
<p className="my-8 text-justify">
72129
This demo illustrates how the{" "}
73130
<a
74131
className="text-blue-600 visited:text-purple-600"
@@ -89,7 +146,8 @@ export default function Home() {
89146
>
90147
IronSession
91148
</a>
92-
. Check the{" "}
149+
. You can choose which ticket fields to reveal during the authentication process by enabling the Developer Mode.
150+
We kindly invite you to check the{" "}
93151
<a
94152
className="text-blue-600 visited:text-purple-600"
95153
href="https://github.com/cedoor/zuauth#readme"
@@ -109,14 +167,31 @@ export default function Home() {
109167
</button>
110168
</div>
111169

112-
{user && <div className="text-center">User: {user.attendeeEmail}</div>}
170+
{!user &&
171+
<>
172+
<div className="my-8 text-center flex flex-col items-center">
173+
<p className="mt-2 text-center">Developer Mode</p>
174+
<Toggle
175+
checked={developerMode}
176+
onToggle={handleSetDeveloperMode}
177+
/>
178+
</div>
113179

114-
<div className="mt-10 text-center">
115-
<a href="https://github.com/cedoor/zuauth" className="underline" target="_blank">
116-
Github
117-
</a>
118-
</div>
180+
<div style={{ height: "300px" }}>
181+
{developerMode && (
182+
<DeveloperPanel
183+
fieldsToReveal={ticketFieldsToReveal}
184+
onToggleField={handleToggleField}
185+
disabled={!!user}
186+
/>
187+
)}
188+
</div>
189+
</>
190+
}
191+
192+
{user && <div className="my-8 text-center">
193+
<DisplayRevealedFields user={user} revealedFields={ticketFieldsToReveal} /> </div>}
119194
</div>
120-
</main>
195+
</main >
121196
)
122-
}
197+
}

example/src/styles/globals.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ body {
2424
rgb(var(--background-end-rgb))
2525
)
2626
rgb(var(--background-start-rgb));
27-
}
27+
}

example/yarn.lock

+10
Original file line numberDiff line numberDiff line change
@@ -4755,6 +4755,15 @@ __metadata:
47554755
languageName: node
47564756
linkType: hard
47574757

4758+
"react-icons@npm:^4.11.0":
4759+
version: 4.11.0
4760+
resolution: "react-icons@npm:4.11.0"
4761+
peerDependencies:
4762+
react: "*"
4763+
checksum: 95e837e11ece80cc39ef1beac026d10f96cd7e567afc718e717517beb35b82dd59307a758c10b3a449dc15d6682d6551ecc630b2821d9365819af921fa279a73
4764+
languageName: node
4765+
linkType: hard
4766+
47584767
"react-is@npm:^16.13.1, react-is@npm:^16.7.0":
47594768
version: 16.13.1
47604769
resolution: "react-is@npm:16.13.1"
@@ -5828,6 +5837,7 @@ __metadata:
58285837
postcss: "npm:^8"
58295838
react: "npm:^18"
58305839
react-dom: "npm:^18"
5840+
react-icons: "npm:^4.11.0"
58315841
tailwindcss: "npm:^3.3.0"
58325842
typescript: "npm:^5"
58335843
zuauth: "npm:0.2.1"

0 commit comments

Comments
 (0)