Skip to content

Commit 95cdda8

Browse files
committed
fix(events): fix check-in QR flow and event id normalization
1 parent 0918b1c commit 95cdda8

5 files changed

Lines changed: 109 additions & 39 deletions

File tree

apps/web/src/app/(main)/(public)/events/[eventId]/checkin/page.tsx

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
useRouter,
2020
useSearchParams,
2121
} from "next/navigation";
22-
import { useEffect, useState } from "react";
22+
import { useCallback, useEffect, useRef, useState } from "react";
2323
import { toast } from "sonner";
2424
import { useTranslations } from "next-intl";
2525

@@ -53,14 +53,12 @@ export default function EventCheckInPage() {
5353
);
5454
const [loading, setLoading] = useState(true);
5555
const [isCheckingIn, setIsCheckingIn] = useState(false);
56+
const hasAutoCheckInTriggeredRef = useRef(false);
57+
const hasAutoLoginRedirectRef = useRef(false);
5658
const searchString = searchParams.toString();
5759
const redirectTo = searchString ? `${pathname}?${searchString}` : pathname;
5860

59-
useEffect(() => {
60-
fetchCheckInStatus();
61-
}, [eventId]);
62-
63-
const fetchCheckInStatus = async () => {
61+
const fetchCheckInStatus = useCallback(async () => {
6462
try {
6563
const response = await fetch(
6664
`/api/events/${eventId}/checkin/status`,
@@ -86,10 +84,16 @@ export default function EventCheckInPage() {
8684
} finally {
8785
setLoading(false);
8886
}
89-
};
87+
}, [eventId, t]);
88+
89+
useEffect(() => {
90+
hasAutoCheckInTriggeredRef.current = false;
91+
hasAutoLoginRedirectRef.current = false;
92+
void fetchCheckInStatus();
93+
}, [fetchCheckInStatus]);
9094

91-
const handleCheckIn = async () => {
92-
if (!checkInStatus?.canCheckIn) {
95+
const handleCheckIn = useCallback(async () => {
96+
if (!checkInStatus?.canCheckIn || isCheckingIn) {
9397
return;
9498
}
9599

@@ -108,7 +112,7 @@ export default function EventCheckInPage() {
108112
if (response.ok) {
109113
toast.success(t("successfullyCheckedIn"));
110114
// Refresh status
111-
fetchCheckInStatus();
115+
await fetchCheckInStatus();
112116
} else {
113117
const error = await response.json();
114118
toast.error(error.error || t("checkInError"));
@@ -119,9 +123,16 @@ export default function EventCheckInPage() {
119123
} finally {
120124
setIsCheckingIn(false);
121125
}
122-
};
126+
}, [
127+
checkInStatus?.canCheckIn,
128+
isCheckingIn,
129+
eventId,
130+
t,
131+
fetchCheckInStatus,
132+
]);
123133

124134
const statusMessageLower = checkInStatus?.message?.toLowerCase() ?? "";
135+
const isLoginRequired = checkInStatus?.statusCode === "LOGIN_REQUIRED";
125136

126137
const isNotRegistered =
127138
checkInStatus?.statusCode === "NOT_REGISTERED" ||
@@ -131,15 +142,26 @@ export default function EventCheckInPage() {
131142
checkInStatus?.statusCode === "REGISTRATION_PENDING" ||
132143
statusMessageLower.includes("not approved");
133144

134-
// If user is not registered, jump straight to the registration entry
135145
useEffect(() => {
136-
if (!checkInStatus || !isNotRegistered) return;
137-
const searchParams = new URLSearchParams();
138-
searchParams.set("openRegistration", "1");
139-
searchParams.set("from", "checkin");
140-
const targetPath = `/events/${eventId}?${searchParams.toString()}`;
141-
router.replace(targetPath);
142-
}, [checkInStatus, isNotRegistered, eventId, router]);
146+
if (!isLoginRequired || hasAutoLoginRedirectRef.current) return;
147+
hasAutoLoginRedirectRef.current = true;
148+
router.replace(
149+
`/auth/login?redirectTo=${encodeURIComponent(redirectTo)}`,
150+
);
151+
}, [isLoginRequired, redirectTo, router]);
152+
153+
useEffect(() => {
154+
if (!checkInStatus?.canCheckIn || checkInStatus.isAlreadyCheckedIn)
155+
return;
156+
if (isCheckingIn || hasAutoCheckInTriggeredRef.current) return;
157+
hasAutoCheckInTriggeredRef.current = true;
158+
void handleCheckIn();
159+
}, [
160+
checkInStatus?.canCheckIn,
161+
checkInStatus?.isAlreadyCheckedIn,
162+
isCheckingIn,
163+
handleCheckIn,
164+
]);
143165

144166
if (loading) {
145167
return (
@@ -314,8 +336,7 @@ export default function EventCheckInPage() {
314336

315337
{!checkInStatus.canCheckIn &&
316338
!checkInStatus.isAlreadyCheckedIn &&
317-
checkInStatus.message ===
318-
t("loginRequiredMessage") && (
339+
isLoginRequired && (
319340
<Button
320341
size="lg"
321342
className="w-full"

apps/web/src/modules/account/events/hooks/useEventManagement.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,10 @@ export function useEventManagement() {
451451
errorType = "warning";
452452
} else if (rawError.includes("not registered")) {
453453
errorMessage = t("actions.checkInNotRegistered");
454-
} else if (rawError.includes("not confirmed")) {
454+
} else if (
455+
rawError.includes("not confirmed") ||
456+
rawError.includes("not approved")
457+
) {
455458
errorMessage = t("actions.checkInNotConfirmed");
456459
}
457460

apps/web/src/server/routes/events.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -948,12 +948,23 @@ app.get("/:eventId/my-feedback", async (c) => {
948948
}
949949

950950
const eventId = c.req.param("eventId");
951+
const event = await getEventById(eventId, { includeAdminData: false });
952+
953+
if (!event) {
954+
return c.json(
955+
{
956+
success: false,
957+
error: "Event not found",
958+
},
959+
404,
960+
);
961+
}
951962

952963
// Get user's feedback for this event
953964
const feedback = await db.eventFeedback.findUnique({
954965
where: {
955966
eventId_userId: {
956-
eventId,
967+
eventId: event.id,
957968
userId: session.user.id,
958969
},
959970
},

apps/web/src/server/routes/events/checkin.ts

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ app.post("/", zValidator("json", checkInSchema), async (c) => {
5656
404,
5757
);
5858
}
59+
const canonicalEventId = event.id;
5960

6061
// Check if event check-in is available (2 hours before start time)
6162
const now = new Date();
@@ -76,7 +77,7 @@ app.post("/", zValidator("json", checkInSchema), async (c) => {
7677
// If checking in someone else, verify permissions
7778
if (data.userId && data.userId !== session.user.id) {
7879
const hasPermission = await canViewEventManagementData(
79-
data.eventId,
80+
canonicalEventId,
8081
session.user.id,
8182
);
8283

@@ -91,8 +92,33 @@ app.post("/", zValidator("json", checkInSchema), async (c) => {
9192
}
9293
}
9394

95+
const registration = await getUserRegistration(
96+
canonicalEventId,
97+
targetUserId,
98+
);
99+
100+
if (!registration) {
101+
return c.json(
102+
{
103+
success: false,
104+
error: "User is not registered for this event",
105+
},
106+
400,
107+
);
108+
}
109+
110+
if (registration.status !== "APPROVED") {
111+
return c.json(
112+
{
113+
success: false,
114+
error: "User is not confirmed for this event yet",
115+
},
116+
400,
117+
);
118+
}
119+
94120
const checkIn = await checkIntoEvent({
95-
eventId: data.eventId,
121+
eventId: canonicalEventId,
96122
userId: targetUserId,
97123
checkedInBy: data.userId ? session.user.id : undefined,
98124
});
@@ -105,7 +131,7 @@ app.post("/", zValidator("json", checkInSchema), async (c) => {
105131
category: "活动参与",
106132
description: `参与活动:${event.title}`,
107133
cpValue: CP_VALUES.EVENT_CHECKIN,
108-
sourceId: data.eventId,
134+
sourceId: canonicalEventId,
109135
sourceType: "event",
110136
organizationId: event.organizationId || undefined,
111137
});
@@ -189,10 +215,11 @@ app.get("/", async (c) => {
189215
404,
190216
);
191217
}
218+
const canonicalEventId = event.id;
192219

193220
// Check if user has permission to view check-ins
194221
const hasPermission = await canViewEventManagementData(
195-
eventId,
222+
canonicalEventId,
196223
session.user.id,
197224
);
198225

@@ -206,7 +233,7 @@ app.get("/", async (c) => {
206233
);
207234
}
208235

209-
const checkIns = await getEventCheckIns(eventId);
236+
const checkIns = await getEventCheckIns(canonicalEventId);
210237

211238
return c.json({
212239
success: true,
@@ -263,10 +290,11 @@ app.get("/status", async (c) => {
263290
404,
264291
);
265292
}
293+
const canonicalEventId = event.id;
266294

267295
// Check if user is registered for the event
268296
const registration = await getUserRegistration(
269-
eventId,
297+
canonicalEventId,
270298
session.user.id,
271299
);
272300
if (!registration) {
@@ -297,7 +325,10 @@ app.get("/status", async (c) => {
297325
}
298326

299327
// Check if user is already checked in
300-
const existingCheckIn = await getUserCheckIn(eventId, session.user.id);
328+
const existingCheckIn = await getUserCheckIn(
329+
canonicalEventId,
330+
session.user.id,
331+
);
301332
if (existingCheckIn) {
302333
return c.json({
303334
success: true,
@@ -400,11 +431,12 @@ app.delete("/", zValidator("json", checkInSchema), async (c) => {
400431
404,
401432
);
402433
}
434+
const canonicalEventId = event.id;
403435

404436
// If canceling someone else's check-in, verify permissions
405437
if (data.userId && data.userId !== session.user.id) {
406438
const hasPermission = await canViewEventManagementData(
407-
data.eventId,
439+
canonicalEventId,
408440
session.user.id,
409441
);
410442

@@ -420,7 +452,7 @@ app.delete("/", zValidator("json", checkInSchema), async (c) => {
420452
}
421453

422454
const canceledCheckIn = await cancelEventCheckIn(
423-
data.eventId,
455+
canonicalEventId,
424456
targetUserId,
425457
);
426458

apps/web/src/server/routes/events/feedback.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ app.post("/", zValidator("json", feedbackSchema), async (c) => {
9292
404,
9393
);
9494
}
95+
const canonicalEventId = event.id;
9596

9697
// Allow feedback for published events (removed time restrictions)
9798
if (
@@ -109,7 +110,7 @@ app.post("/", zValidator("json", feedbackSchema), async (c) => {
109110

110111
// Check if user is registered for the event with confirmed status
111112
const registration = await getEventRegistration(
112-
eventId,
113+
canonicalEventId,
113114
session.user.id,
114115
);
115116
if (!registration || registration.status !== "APPROVED") {
@@ -162,7 +163,7 @@ app.post("/", zValidator("json", feedbackSchema), async (c) => {
162163
}
163164

164165
const feedback = await createEventFeedback({
165-
eventId,
166+
eventId: canonicalEventId,
166167
userId: session.user.id,
167168
rating: data.rating,
168169
comment: data.comment || null,
@@ -179,7 +180,7 @@ app.post("/", zValidator("json", feedbackSchema), async (c) => {
179180
category: "活动参与",
180181
description: `为活动"${event.title}"提供反馈`,
181182
cpValue: CP_VALUES.EVENT_FEEDBACK,
182-
sourceId: eventId,
183+
sourceId: canonicalEventId,
183184
sourceType: "event_feedback",
184185
organizationId: event.organizationId || undefined,
185186
});
@@ -257,10 +258,11 @@ app.get("/", async (c) => {
257258
404,
258259
);
259260
}
261+
const canonicalEventId = event.id;
260262

261263
// Check if user has permission to view feedback
262264
const hasPermission = await canViewEventManagementData(
263-
eventId,
265+
canonicalEventId,
264266
session.user.id,
265267
);
266268
if (!hasPermission) {
@@ -273,7 +275,7 @@ app.get("/", async (c) => {
273275
);
274276
}
275277

276-
const feedback = await getEventFeedback(eventId);
278+
const feedback = await getEventFeedback(canonicalEventId);
277279

278280
return c.json({
279281
success: true,
@@ -332,10 +334,11 @@ app.put("/", zValidator("json", updateFeedbackSchema), async (c) => {
332334
404,
333335
);
334336
}
337+
const canonicalEventId = event.id;
335338

336339
// Check if user is registered for the event with confirmed status
337340
const registration = await getEventRegistration(
338-
eventId,
341+
canonicalEventId,
339342
session.user.id,
340343
);
341344
if (!registration || registration.status !== "APPROVED") {
@@ -388,7 +391,7 @@ app.put("/", zValidator("json", updateFeedbackSchema), async (c) => {
388391
}
389392

390393
const updatedFeedback = await updateEventFeedback(
391-
eventId,
394+
canonicalEventId,
392395
session.user.id,
393396
{
394397
rating: data.rating,

0 commit comments

Comments
 (0)