import { router } from "expo-router"; import { useEffect, useRef, useState } from "react"; import { Alert, Platform } from "react-native"; import { useAccounts } from "@/contexts/AccountsContext"; import { useAppLock } from "@/contexts/AppLockContext"; import { clearPendingShortcut, peekPendingShortcut, subscribeShortcutQueue, } from "@/lib/shortcut-queue"; import { DEFAULT_CLOCK_DESCRIPTION, resolveClockDescription, resolveEffectiveHourlyRate, } from "@/lib/time-clock"; import { getLastTimeClockClientId } from "@/lib/time-clock-prefs"; import type { ParsedShortcut } from "@/lib/shortcuts"; import { api } from "@/lib/trpc"; /** * Executes queued shortcut actions once the user is signed in, unlocked, and data is ready. */ export function ShortcutHandler() { const { activeAccountId } = useAccounts(); const { isLocked } = useAppLock(); const utils = api.useUtils(); const clientsQuery = api.clients.getAll.useQuery(); const runningQuery = api.timeEntries.getRunning.useQuery(); const [pending, setPending] = useState(null); const processingRef = useRef(false); const clockIn = api.timeEntries.clockIn.useMutation(); const clockOut = api.timeEntries.clockOut.useMutation(); useEffect(() => { let cancelled = false; async function refresh() { const next = await peekPendingShortcut(); if (!cancelled) setPending(next); } void refresh(); return subscribeShortcutQueue(() => { void refresh(); }); }, []); useEffect(() => { if (!pending || !activeAccountId || isLocked) return; if (clientsQuery.isLoading || runningQuery.isLoading) return; if (processingRef.current) return; processingRef.current = true; void (async () => { try { if (pending.action === "open-timer") { await clearPendingShortcut(); setPending(null); router.push("/(app)/timer"); return; } if (pending.action === "clock-out") { if (!runningQuery.data) { await clearPendingShortcut(); setPending(null); router.push("/(app)/timer"); if (Platform.OS === "ios") { Alert.alert("No timer running", "There is nothing to clock out."); } return; } await clockOut.mutateAsync({}); await Promise.all([ utils.timeEntries.getRunning.invalidate(), utils.timeEntries.getAll.invalidate(), utils.invoices.getAll.invalidate(), utils.dashboard.getStats.invalidate(), ]); await clearPendingShortcut(); setPending(null); router.push("/(app)/timer"); return; } if (pending.action === "clock-in") { if (runningQuery.data) { await clearPendingShortcut(); setPending(null); router.push("/(app)/timer"); if (Platform.OS === "ios") { Alert.alert("Timer already running", "Stop the current timer before clocking in again."); } return; } const clientId = pending.clientId || (await getLastTimeClockClientId(activeAccountId)) || ""; if (!clientId) { await clearPendingShortcut(); setPending(null); router.push("/(app)/timer"); Alert.alert( "Choose a client", "Open the time clock and pick a client once — shortcuts will use it next time.", ); return; } const client = (clientsQuery.data ?? []).find((entry) => entry.id === clientId); const rate = resolveEffectiveHourlyRate("", client?.defaultHourlyRate); await clockIn.mutateAsync({ clientId, description: resolveClockDescription(pending.title || DEFAULT_CLOCK_DESCRIPTION), rate: rate ?? undefined, }); await utils.timeEntries.getRunning.invalidate(); await clearPendingShortcut(); setPending(null); router.push("/(app)/timer"); } } catch (err) { await clearPendingShortcut(); setPending(null); Alert.alert( pending.action === "clock-out" ? "Clock out failed" : "Clock in failed", err instanceof Error ? err.message : "Something went wrong.", ); router.push("/(app)/timer"); } finally { processingRef.current = false; } })(); }, [ activeAccountId, clockIn, clockOut, clientsQuery.data, clientsQuery.isLoading, isLocked, pending, runningQuery.data, runningQuery.isLoading, utils, ]); return null; }