All files / src/features/Events/EventDetail/tickets useTicketCheckIn.ts

100% Statements 32/32
100% Branches 4/4
100% Functions 11/11
100% Lines 28/28

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                                    13x 13x   13x 12x 12x       13x                   13x 55x 55x 55x       55x 55x 36x 36x 36x               55x 6x 5x 5x   1x                               1x 1x         55x 10x 8x     6x 6x         55x    
import { useEffect, useState } from "react";
import { useQueryClient } from "react-query";
import NiceModal from "@ebay/nice-modal-react";
import { showMessage } from "@common/Toast";
import { QKEY_EVENT_TICKETS } from "@constants/queryKeys";
import { useCheckInEventTickets } from "@services/api/products/tickets";
import { useCustomThemeV2 } from "@theme/hooks/useCustomThemeV2";
import { EVENT_TICKET_CHECK_IN_MODAL } from "modals/modal_names";
import { CHECK_IN_SUCCESS_TOAST } from "./EventTicketCheckInModal";
import { EventTicketRow } from "./ticket.types";
 
// ONE lock for the whole surface, not one per hook instance: the table row and
// the detail panel each mount this hook (the panel's modal args are fixed by
// `useTable`, so it cannot borrow the tab's instance), and a per-instance lock
// left the row's "Check In" clickable beside the panel while the panel's
// request was in flight. Check-in is one-way on the API — a duplicate PATCH is
// not undoable — so the guard must be a plain variable read synchronously:
// React state re-renders too late for two clicks landing in the same frame.
let activeCheckInId: EventTicketRow["id"] | null = null;
const lockListeners = new Set<(id: EventTicketRow["id"] | null) => void>();
 
const setActiveCheckInId = (id: EventTicketRow["id"] | null) => {
  activeCheckInId = id;
  lockListeners.forEach((notify) => notify(id));
};
 
/** Test-only: clears the shared lock a case may have left held. */
export const resetTicketCheckInLock = () => setActiveCheckInId(null);
 
/**
 * Manual check-in, confirmation and all (frames 7412-195380 / 7412-197026).
 *
 * Shared by the table's row button and the detail panel so both go through the
 * same confirm → mutate → toast sequence and neither can skip the confirmation.
 * The mutation lives here rather than in the modal because the modal is dismissed
 * the moment it is confirmed — the caller's own row/panel shows the pending state.
 */
export const useTicketCheckIn = (eventId?: string) => {
  const queryClient = useQueryClient();
  const { isMobileView } = useCustomThemeV2();
  const mutation = useCheckInEventTickets(eventId);
 
  // The shared lock mirrored into state so every mounted caller re-renders its
  // disabled/pending UI when any of them starts or finishes a check-in.
  const [checkingInId, setCheckingInId] = useState(activeCheckInId);
  useEffect(() => {
    lockListeners.add(setCheckingInId);
    return () => {
      lockListeners.delete(setCheckingInId);
    };
  }, []);
 
  // Bulk endpoint with a single-element array (see tickets.ts). The list is
  // refetched on settle either way — on failure (e.g. the ticket was scanned
  // elsewhere in the meantime) the refetch resyncs the row's real state, and the
  // axios interceptor already surfaces the error toast.
  const checkIn = (row: EventTicketRow) => {
    if (activeCheckInId !== null) return;
    setActiveCheckInId(row.id);
    mutation.mutate([row.id], {
      onSuccess: () => {
        showMessage(
          "Success",
          CHECK_IN_SUCCESS_TOAST,
          !isMobileView,
          "",
          undefined,
          undefined,
          // Frame 7412-197026 is a single line: the message itself, no "Success"
          // heading above it.
          { showTitle: false },
        );
      },
      onSettled: async () => {
        // Hold the lock through the refetch, not just the mutation: released on
        // settle, "Check In" came back for the beat before `checkedInAt` landed
        // on the row — long enough for a second, duplicate submit.
        await queryClient.invalidateQueries(QKEY_EVENT_TICKETS);
        setActiveCheckInId(null);
      },
    });
  };
 
  const requestCheckIn = (row: EventTicketRow) => {
    if (activeCheckInId !== null) return;
    NiceModal.show(EVENT_TICKET_CHECK_IN_MODAL, {
      row,
      onConfirm: (confirmed: EventTicketRow) => {
        NiceModal.remove(EVENT_TICKET_CHECK_IN_MODAL);
        checkIn(confirmed);
      },
    });
  };
 
  return { requestCheckIn, checkingInId };
};