Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | 1x 1x 1x 1x 1x 1x 15x 15x 15x 15x 15x 15x 15x 15x 15x 5x 1x 1x 1x 1x 4x 4x 4x 4x 4x 4x 15x 5x 15x 3x 3x 3x 1x 1x 2x 2x 2x 2x 2x 2x 2x 1x 1x 1x 1x 1x 2x 2x 15x 15x 1x 1x 1x 15x | import { useCallback, useEffect, useRef, useState } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { ApiError } from '@/services/api';
import { getEventDetail } from '@/services/eventService';
import { scanTicket } from '@/services/ticketService';
import type { EventDetail } from '@/models/event';
import type { TicketScanResponse } from '@/models/ticket';
export interface TicketScanViewModel {
event: EventDetail | null;
qrToken: string;
scanResult: TicketScanResponse | null;
isLoading: boolean;
isSubmitting: boolean;
errorMessage: string | null;
isHost: boolean;
setQrToken: (value: string) => void;
submit: () => Promise<void>;
submitToken: (value: string) => Promise<void>;
clearResult: () => void;
reload: () => Promise<void>;
}
function getLoadErrorMessage(error: unknown): string {
Iif (error instanceof ApiError && error.status === 401) {
return 'You must be logged in to scan tickets.';
}
Iif (error instanceof ApiError) {
return error.message;
}
return 'Failed to load the scan screen. Please try again.';
}
export function useTicketScanViewModel(eventId: string): TicketScanViewModel {
const { token } = useAuth();
const [event, setEvent] = useState<EventDetail | null>(null);
const [qrToken, setQrToken] = useState('');
const [scanResult, setScanResult] = useState<TicketScanResponse | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const isSubmittingRef = useRef(false);
const reload = useCallback(async () => {
if (!token) {
setEvent(null);
setErrorMessage('You must be logged in to scan tickets.');
setIsLoading(false);
return;
}
setIsLoading(true);
setErrorMessage(null);
try {
const eventDetail = await getEventDetail(eventId, token);
setEvent(eventDetail);
} catch (error) {
setEvent(null);
setErrorMessage(getLoadErrorMessage(error));
} finally {
setIsLoading(false);
}
}, [eventId, token]);
useEffect(() => {
void reload();
}, [reload]);
const submitToken = useCallback(async (value: string) => {
Iif (!token || isSubmittingRef.current) return;
const trimmed = value.trim();
if (!trimmed) {
setErrorMessage('Enter a QR token before validating the ticket.');
return;
}
isSubmittingRef.current = true;
setIsSubmitting(true);
setErrorMessage(null);
setScanResult(null);
setQrToken(trimmed);
try {
const result = await scanTicket(eventId, trimmed, token);
setScanResult(result);
} catch (error) {
// If we already have a scan result from a parallel request that succeeded,
// don't overwrite it with an error (like ALREADY_USED)
setScanResult((prev) => {
Iif (prev?.result === 'ACCEPTED') return prev;
setErrorMessage(
error instanceof ApiError ? error.message : 'Failed to validate the ticket.',
);
return null;
});
} finally {
setIsSubmitting(false);
isSubmittingRef.current = false;
}
}, [eventId, token]);
const submit = useCallback(async () => {
await submitToken(qrToken);
}, [qrToken, submitToken]);
const clearResult = useCallback(() => {
setScanResult(null);
setErrorMessage(null);
setQrToken('');
}, []);
return {
event,
qrToken,
scanResult,
isLoading,
isSubmitting,
errorMessage,
isHost: Boolean(event?.viewer_context.is_host),
setQrToken,
submit,
submitToken,
clearResult,
reload,
};
}
|