Skip to content

Commit 80af64e

Browse files
committed
zupass fallback
1 parent 2c0db24 commit 80af64e

11 files changed

Lines changed: 470 additions & 25 deletions

File tree

devconnect-app/src/app/map/venue-map/VenueMap2.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ export const VenueMap = () => {
294294
ref={containerRef}
295295
id="venue-container"
296296
className={cn(
297-
'relative w-full overflow-hidden grow flex'
297+
'relative w-full overflow-hidden grow flex py-8'
298298
// hasActiveFilters && css['has-selection-or-hover']
299299
)}
300300
onClick={(e) => {

devconnect-app/src/app/wallet/TicketTab.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export default function TicketTab() {
3636

3737
// Auto-load tickets when component mounts
3838
useEffect(() => {
39-
const fetchTickets = async (forceRefresh = false) => {
39+
const fetchTickets = async (forceRefresh = true) => {
4040
if (isLoadingRef.current) {
4141
console.log('Already fetching tickets, skipping...');
4242
return;
@@ -65,10 +65,13 @@ export default function TicketTab() {
6565
const ticketsData = response.data.tickets || [];
6666
setTickets(ticketsData);
6767

68+
console.log('ticketsData', ticketsData);
69+
6870
// Generate QR codes for each ticket
6971
const newQrCodes: { [key: string]: string } = {};
7072
for (const order of ticketsData) {
7173
for (const ticket of order.tickets) {
74+
console.log('ticket', ticket);
7275
if (ticket.secret) {
7376
try {
7477
const qrDataUrl = await QRCode.toDataURL(ticket.secret, {

devconnect/src/common/components/perks/perks-list.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type Perk = {
2626
wallet_proof?: boolean
2727
global_coupon?: string
2828
instructions?: string
29+
zupass_disabled?: boolean
2930
}
3031

3132
const perks: Perk[] = [

devconnect/src/pages/api/coupons/[...collection].tsx

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { POD } from '@pcd/pod'
1414
import { PODData } from '@parcnet-js/podspec'
1515
import { proveWalletOwnership } from './wallet-proof'
1616
import { eventShops } from 'lib/components/event-schedule-new/zupass/event-shops-list'
17+
import { supabase } from 'common/supabaseClient'
18+
import { getPaidTicketsByEmail } from './pretix'
1719

1820
const verifyPodSignature = (podData: PODData): boolean => {
1921
try {
@@ -58,6 +60,54 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
5860
perksList.find(perk => perk.coupon_collection === collection) ||
5961
eventShops.find(event => event.coupon_collection === collection)
6062

63+
// Zupass gating refers to third party event shops that use zupass to gate their event via our calendar
64+
const isZupassGating = eventShops.some(event => event.coupon_collection === collection)
65+
66+
if (isZupassGating && perk?.zupass_disabled) {
67+
console.warn('ZUPASS SKIPPED, USING SUPABASE AUTH INSTEAD: ', collection)
68+
69+
// Extract token from Authorization header
70+
const authHeader = req.headers.authorization
71+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
72+
return res.status(401).json({ error: 'Missing or invalid Authorization header' })
73+
}
74+
75+
const token = authHeader.substring(7) // Remove 'Bearer ' prefix
76+
77+
// Verify supabase token
78+
const {
79+
data: { user },
80+
error: authError,
81+
} = await supabase.auth.getUser(token)
82+
83+
if (authError || !user) {
84+
return res.status(401).json({ error: 'Invalid or expired Supabase token' })
85+
}
86+
87+
const claimedBy = user.email
88+
89+
if (!claimedBy) {
90+
return res.status(400).json({ error: 'User email is required' })
91+
}
92+
93+
const tickets = await getPaidTicketsByEmail(claimedBy)
94+
95+
const hasTickets = tickets && tickets.length > 0
96+
97+
if (!hasTickets) {
98+
return res.status(400).json({ error: 'User does not have a paid ticket' })
99+
}
100+
101+
const coupon = await claimSingleCoupon(perk!.zupass_proof_id ?? '', collection, claimedBy)
102+
103+
return res.status(200).json({
104+
coupon: coupon.coupon,
105+
coupon_status: coupon.status,
106+
collection,
107+
ticket_type: perk!.zupass_proof_id,
108+
})
109+
}
110+
61111
if (!perk) {
62112
return res.status(400).json({ error: 'Perk or gated event not found' })
63113
}
@@ -130,11 +180,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
130180
}
131181

132182
const ticketId = pod.entries.ticketId?.value?.toString()
183+
const ticketOwner = pod.entries.attendeeEmail?.value?.toString()
133184

134-
if (!ticketId) {
135-
return res.status(400).json({ error: 'Ticket ID is required' })
185+
if (!ticketId || !ticketOwner) {
186+
return res.status(400).json({ error: 'Ticket ID and ticket owner are required' })
136187
}
137188

189+
// If perk is same for everyone, just return it, no need to claim
138190
if (perk.global_coupon) {
139191
return res.status(200).json({
140192
coupon: perk.global_coupon,
@@ -144,7 +196,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
144196
})
145197
}
146198

147-
const coupon = await claimSingleCoupon(perk.zupass_proof_id ?? '', collection, ticketId)
199+
// If zupass gating, use ticket owner, otherwise use ticket ID. Why:
200+
// This is because the ticket id is zupass specific and not known to pretix - we are building a fallback claiming process if zupass goes down - that means we cannot claim by the ticket id found in the proof, we have to use something that works in either case (email)
201+
const claimedBy = isZupassGating ? ticketOwner : ticketId
202+
const coupon = await claimSingleCoupon(perk.zupass_proof_id ?? '', collection, claimedBy)
148203

149204
return res.status(200).json({
150205
coupon: coupon.coupon,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
export async function getPaidTicketsByEmail(email: string) {
2+
const apiKey = process.env.PRETIX_API_KEY
3+
const baseUrl = process.env.PRETIX_BASE_URL || 'https://ticketh.xyz'
4+
const organizerSlug = 'devconnect'
5+
const eventSlug = 'cowork'
6+
7+
if (!apiKey) {
8+
throw new Error('PRETIX_API_KEY is missing')
9+
}
10+
11+
if (!organizerSlug || !eventSlug) {
12+
throw new Error('Missing Pretix configuration in environment variables')
13+
}
14+
15+
const url = `${baseUrl}/api/v1/organizers/${organizerSlug}/events/${eventSlug}/orders`
16+
17+
const params = new URLSearchParams({
18+
email: email,
19+
status: 'p', // 'p' for paid orders
20+
})
21+
22+
const response = await fetch(`${url}?${params}`, {
23+
method: 'GET',
24+
headers: {
25+
'Authorization': `Token ${apiKey}`,
26+
'Content-Type': 'application/json',
27+
},
28+
})
29+
30+
if (!response.ok) {
31+
console.error('Pretix API error:', response)
32+
throw new Error(`Pretix API error: ${response.status}`)
33+
}
34+
35+
const data = await response.json()
36+
const orders = data.results || []
37+
38+
// Return simplified ticket data, filtering out positions without attendee names
39+
return orders.map((order: any) => ({
40+
orderCode: order.code,
41+
orderDate: order.datetime,
42+
email: order.email,
43+
tickets: order.positions
44+
.filter((position: any) => position.attendee_name && position.attendee_name.trim() !== '')
45+
.map((position: any) => ({
46+
secret: position.secret,
47+
attendeeName: position.attendee_name,
48+
attendeeEmail: position.attendee_email || order.email,
49+
price: position.price,
50+
itemName: position.item_name || 'Ticket',
51+
}))
52+
}))
53+
}
54+
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
type EventShop = {
22
supabase_id: string
33
coupon_collection: string
4-
zupass_proof_id?: string
4+
zupass_proof_id?: string
55
custom_url_id?: string
66
global_coupon?: string
7+
zupass_disabled?: boolean
78
}
89

10+
const zupassGatingFallbackOn = process.env.NEXT_PUBLIC_ZUPASS_FALLBACK_ON === "true"
11+
912
export const eventShops: EventShop[] = [
1013
{
1114
supabase_id: "86",
1215
custom_url_id: 'DSS',
1316
zupass_proof_id: "Devconnect ARG",
1417
coupon_collection: "dss-voucher-gating",
18+
zupass_disabled: zupassGatingFallbackOn,
1519
},
1620
];

0 commit comments

Comments
 (0)