Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions frontend/src/components/EventCheckIn/CheckInList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { formatDistanceToNow } from 'date-fns';
import './EventCheckIn.scss';

function getAttendeeDisplayName(attendee) {
if (attendee.anonymousBrowserCheckIn) {
return attendee.guestName || 'Anonymous user';
}
if (attendee.formResponseId && (attendee.guestName || attendee.guestEmail)) {
return attendee.guestName && String(attendee.guestName).trim()
? String(attendee.guestName).trim()
Expand Down Expand Up @@ -49,12 +52,14 @@ function CheckInList({ attendees, onManualCheckIn, onRemoveCheckIn, onOpenManual
};

const getUserPicture = (attendee) => {
if (attendee.anonymousBrowserCheckIn) return null;
if (attendee.formResponseId) return null;
const user = attendee.userId;
return user?.picture || null;
};

const getAttendeeRemoveId = (attendee) => {
if (attendee.anonymousBrowserCheckIn) return null;
if (attendee.formResponseId) return { formResponseId: attendee.formResponseId };
const uid = attendee.userId && (attendee.userId._id || attendee.userId.id || attendee.userId);
return uid ? { userId: uid } : null;
Expand Down Expand Up @@ -117,9 +122,10 @@ function CheckInList({ attendees, onManualCheckIn, onRemoveCheckIn, onOpenManual
const user = attendee.userId;
const checkedInBy = attendee.checkedInBy;
const isManual = !!checkedInBy;
const isAnonymousBrowser = !!attendee.anonymousBrowserCheckIn;
const removeId = getAttendeeRemoveId(attendee);
return (
<div key={attendee.formResponseId ? `anon-${attendee.formResponseId}` : (user?._id || user?.id || index)} className="attendee-item">
<div key={attendee.formResponseId ? `anon-${attendee.formResponseId}` : (isAnonymousBrowser ? `browser-${attendee.browserIndex || index}` : (user?._id || user?.id || index))} className="attendee-item">
<div className="attendee-info">
<div className="attendee-avatar">
{getUserPicture(attendee) ? (
Expand All @@ -140,9 +146,9 @@ function CheckInList({ attendees, onManualCheckIn, onRemoveCheckIn, onOpenManual
<Icon icon="mdi:clock-outline" />
{formatCheckInTime(attendee.checkedInAt)}
</span>
<span className={`checkin-method ${isManual ? 'manual' : 'self'}`}>
<Icon icon={isManual ? 'mdi:account-check' : 'mdi:qrcode-scan'} />
{isManual ? 'Manual' : 'Self Check-In'}
<span className={`checkin-method ${isAnonymousBrowser ? 'self' : (isManual ? 'manual' : 'self')}`}>
<Icon icon={isAnonymousBrowser ? 'mdi:incognito' : (isManual ? 'mdi:account-check' : 'mdi:qrcode-scan')} />
{isAnonymousBrowser ? 'Anonymous User' : (isManual ? 'Manual' : 'Self Check-In')}
</span>
</div>
</div>
Expand Down
49 changes: 34 additions & 15 deletions frontend/src/components/EventCheckInButton/EventCheckInButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import apiRequest from '../../utils/postRequest';
import { analytics } from '../../services/analytics/analytics';
import './EventCheckInButton.scss';

const ANON_EVENT_CHECKIN_KEY_PREFIX = 'meridian_anon_event_checked_in_';

function getAnonEventCheckInKey(eventId) {
return `${ANON_EVENT_CHECKIN_KEY_PREFIX}${eventId}`;
}

/**
* Check-in button for the event page. Shown when:
* - Event has check-in enabled
Expand All @@ -20,11 +26,21 @@ function EventCheckInButton({ event, onCheckedIn }) {
const { user } = useAuth();
const { addNotification } = useNotification();
const [loading, setLoading] = useState(false);
const [checkedIn, setCheckedIn] = useState(!!event?.currentUserCheckedIn);
const [checkedIn, setCheckedIn] = useState(false);

useEffect(() => {
setCheckedIn(!!event?.currentUserCheckedIn);
}, [event?.currentUserCheckedIn, event?._id]);
const loggedInCheckedIn = !!event?.currentUserCheckedIn;
if (loggedInCheckedIn) {
setCheckedIn(true);
return;
}
if (!user && event?._id && event?.checkInSettings?.fullyAnonymousCheckIn) {
const anonCheckedIn = localStorage.getItem(getAnonEventCheckInKey(event._id)) === '1';
setCheckedIn(anonCheckedIn);
return;
}
setCheckedIn(false);
}, [event?.currentUserCheckedIn, event?._id, event?.checkInSettings?.fullyAnonymousCheckIn, user]);

if (!event) return null;
if (!event.checkInEnabled) return null;
Expand Down Expand Up @@ -71,22 +87,25 @@ function EventCheckInButton({ event, onCheckedIn }) {
};

if (checkedIn) {
const showCheckOut = !!user;
return (
<div className="event-check-in-button checked-in">
<Icon icon="mdi:check-circle" />
<span>You're checked in</span>
<button
type="button"
className="check-out-btn"
onClick={handleCheckOut}
disabled={loading}
>
{loading ? (
<Icon icon="mdi:loading" className="spinner" />
) : (
'Check out'
)}
</button>
{showCheckOut && (
<button
type="button"
className="check-out-btn"
onClick={handleCheckOut}
disabled={loading}
>
{loading ? (
<Icon icon="mdi:loading" className="spinner" />
) : (
'Check out'
)}
</button>
)}
</div>
);
}
Expand Down
36 changes: 35 additions & 1 deletion frontend/src/pages/CheckIn/CheckInConfirmation.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@ function HighlightMatch({ text, query }) {

const APP_STORE_URL = 'https://apps.apple.com/us/app/meridian-go/id6755217537';
const PLAY_STORE_URL = 'https://play.google.com/store/apps/details?id=com.meridian.mobile';
const ANON_CHECKIN_BROWSER_KEY = 'meridian_anon_checkin_browser_id';
const ANON_EVENT_CHECKIN_KEY_PREFIX = 'meridian_anon_event_checked_in_';

function getAnonymousBrowserId() {
if (typeof window === 'undefined') return null;
const existing = localStorage.getItem(ANON_CHECKIN_BROWSER_KEY);
if (existing) return existing;
const generated = `anon-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
localStorage.setItem(ANON_CHECKIN_BROWSER_KEY, generated);
return generated;
}

function markAnonymousEventCheckedIn(eventId) {
if (!eventId || typeof window === 'undefined') return;
localStorage.setItem(`${ANON_EVENT_CHECKIN_KEY_PREFIX}${eventId}`, '1');
}

function useDeviceDetection() {
return useMemo(() => {
Expand Down Expand Up @@ -64,8 +80,9 @@ function CheckInConfirmation() {

const event = eventResponse?.event;
const useSelfCheckIn = !token && user;
const useFullyAnonymousCheckIn = Boolean(token && !user && event?.checkInSettings?.fullyAnonymousCheckIn);
// When token is present, use pick flow for everyone (avoids "must register" for anonymous registrants who later logged in)
const useAnonymousPick = !!token;
const useAnonymousPick = !!token && !useFullyAnonymousCheckIn;

// Registrations for anonymous pick (token flow, no login)
const { data: registrationsResponse, loading: loadingRegistrations, refetch: refetchRegistrations } = useFetch(
Expand Down Expand Up @@ -183,10 +200,19 @@ function CheckInConfirmation() {
try {
const response = useSelfCheckIn
? await apiRequest(`/events/${eventId}/check-in/self`, {}, { method: 'POST' })
: useFullyAnonymousCheckIn
? await apiRequest(
`/events/${eventId}/check-in/anonymous`,
{ token, anonymousBrowserId: getAnonymousBrowserId() },
{ method: 'POST' }
)
: await apiRequest(`/events/${eventId}/check-in`, { token }, { method: 'POST' });

if (response.success) {
setCheckedIn(true);
if (!user) {
markAnonymousEventCheckedIn(eventId);
}
addNotification({
title: 'Success',
message: 'You have successfully checked in!',
Expand Down Expand Up @@ -220,6 +246,9 @@ function CheckInConfirmation() {
const response = await apiRequest(`/events/${eventId}/check-in/anonymous`, body, { method: 'POST' });
if (response.success) {
setCheckedIn(true);
if (!user) {
markAnonymousEventCheckedIn(eventId);
}
addNotification({
title: 'Success',
message: 'You have successfully checked in!',
Expand Down Expand Up @@ -593,6 +622,11 @@ function CheckInConfirmation() {
)}
</div>
<div className="checkin-card__actions">
{useFullyAnonymousCheckIn && (
<p className="checkin-card__anonymous-note">
Anonymous check-in is enabled for this event. This anonymous user can check in once without sharing personal details.
</p>
)}
<button
className="checkin-card__btn checkin-card__btn--primary"
onClick={handleCheckIn}
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/pages/CheckIn/CheckInConfirmation.scss
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,18 @@
gap: 10px;
}

&__anonymous-note {
margin: 0;
font-size: 13px;
line-height: 1.4;
color: var(--text-secondary, #6b7280);
background: #f8fafc;
border: 1px dashed var(--border-color, #dbe3ee);
border-radius: 10px;
padding: 10px 12px;
text-align: left;
}

&__btn {
width: 100%;
padding: 14px 20px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ function CheckInSettingsModal({ isOpen, onClose, event, orgId, onSaved, color })
autoCheckIn: false,
allowEarlyCheckIn: false,
allowAnonymousCheckIn: false,
fullyAnonymousCheckIn: false,
notificationEmailQuestionId: null
});
const [saving, setSaving] = useState(false);
Expand All @@ -32,10 +33,11 @@ function CheckInSettingsModal({ isOpen, onClose, event, orgId, onSaved, color })
autoCheckIn: event.checkInSettings?.autoCheckIn || false,
allowEarlyCheckIn: event.checkInSettings?.allowEarlyCheckIn || false,
allowAnonymousCheckIn: event.checkInSettings?.allowAnonymousCheckIn || false,
fullyAnonymousCheckIn: event.checkInSettings?.fullyAnonymousCheckIn || false,
notificationEmailQuestionId: event.notificationEmailQuestionId ?? null
});
}
}, [isOpen, event?.checkInSettings?.method, event?.checkInSettings?.allowOnPageCheckIn, event?.checkInSettings?.requireRegistration, event?.checkInSettings?.autoCheckIn, event?.checkInSettings?.allowEarlyCheckIn, event?.checkInSettings?.allowAnonymousCheckIn, event?.notificationEmailQuestionId]);
}, [isOpen, event?.checkInSettings?.method, event?.checkInSettings?.allowOnPageCheckIn, event?.checkInSettings?.requireRegistration, event?.checkInSettings?.autoCheckIn, event?.checkInSettings?.allowEarlyCheckIn, event?.checkInSettings?.allowAnonymousCheckIn, event?.checkInSettings?.fullyAnonymousCheckIn, event?.notificationEmailQuestionId]);

const handleSave = async () => {
if (!orgId || !event?._id) return;
Expand All @@ -50,7 +52,8 @@ function CheckInSettingsModal({ isOpen, onClose, event, orgId, onSaved, color })
requireRegistration: form.requireRegistration,
autoCheckIn: form.autoCheckIn,
allowEarlyCheckIn: form.allowEarlyCheckIn,
allowAnonymousCheckIn: form.allowAnonymousCheckIn
allowAnonymousCheckIn: form.allowAnonymousCheckIn,
fullyAnonymousCheckIn: form.fullyAnonymousCheckIn
},
notificationEmailQuestionId: form.notificationEmailQuestionId || null
},
Expand Down Expand Up @@ -135,6 +138,17 @@ function CheckInSettingsModal({ isOpen, onClose, event, orgId, onSaved, color })
/>
)
},
{
title: 'Fully anonymous check-in',
subtitle: 'Allow any anonymous user to check in once with no personal details collected (count only).',
action: (
<input
type="checkbox"
checked={form.fullyAnonymousCheckIn}
onChange={(e) => setForm(s => ({ ...s, fullyAnonymousCheckIn: e.target.checked }))}
/>
)
},
...(event?.registrationFormId
? [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,11 @@ function EventCheckInTab({ event, orgId, onRefresh, isTabActive = false, color }
value={`${stats.checkInRate}%`}
icon="mdi:chart-line"
/>
<KpiCard
title="Anonymous User Check-Ins"
value={stats.anonymousBrowserCheckIns ?? 0}
icon="mdi:incognito"
/>
</div>
)}

Expand Down
2 changes: 1 addition & 1 deletion private-deps.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"events": {
"repo": "git@github.com:Study-Compass/Events-Backend.git",
"ref": "0cb405c8e30676a6a1e1f6dbc0ecc4893aa3edec",
"ref": "5dd303c923c71b7d6b70dbba8273c6fa530ef91e",
"dest": "backend/events"
}
}