import * as Linking from "expo-linking"; import { router } from "expo-router"; import { useEffect, useRef } from "react"; import { Alert, Platform } from "react-native"; import { useAccounts } from "@/contexts/AccountsContext"; import { useAppLock } from "@/contexts/AppLockContext"; import { DEFAULT_CLOCK_DESCRIPTION, resolveClockDescription, resolveEffectiveHourlyRate } from "@/lib/time-clock"; import { getLastTimeClockClientId } from "@/lib/time-clock-prefs"; import { parseShortcutUrl, type ParsedShortcut } from "@/lib/shortcuts"; import { api } from "@/lib/trpc"; /** * Handles deep links from the Shortcuts app, Siri, and Live Activities. * Mounted inside the authenticated app shell. */ export function ShortcutHandler() { const { activeAccountId } = useAccounts(); const { isLocked } = useAppLock(); const url = Linking.useURL(); const utils = api.useUtils(); const clientsQuery = api.clients.getAll.useQuery(); const runningQuery = api.timeEntries.getRunning.useQuery(); const processedRef = useRef(null); const pendingRef = useRef(null); const clockIn = api.timeEntries.clockIn.useMutation(); const clockOut = api.timeEntries.clockOut.useMutation(); useEffect(() => { void Linking.getInitialURL().then((initialUrl) => { const parsed = parseShortcutUrl(initialUrl); if (parsed) pendingRef.current = parsed; }); }, []); useEffect(() => { const parsed = parseShortcutUrl(url); if (parsed) pendingRef.current = parsed; }, [url]); useEffect(() => { if (isLocked || !activeAccountId || clientsQuery.isLoading || runningQuery.isLoading) return; const pending = pendingRef.current; if (!pending) return; const key = JSON.stringify(pending); if (processedRef.current === key) return; processedRef.current = key; pendingRef.current = null; void (async () => { if (pending.action === "open-timer") { router.push("/(app)/timer"); return; } if (pending.action === "clock-out") { if (!runningQuery.data) { router.push("/(app)/timer"); if (Platform.OS === "ios") { Alert.alert("No timer running", "There is nothing to clock out."); } return; } try { await clockOut.mutateAsync({}); await Promise.all([ utils.timeEntries.getRunning.invalidate(), utils.timeEntries.getAll.invalidate(), utils.invoices.getAll.invalidate(), utils.dashboard.getStats.invalidate(), ]); router.push("/(app)/timer"); } catch (err) { Alert.alert( "Clock out failed", err instanceof Error ? err.message : "Could not stop the timer.", ); router.push("/(app)/timer"); } return; } if (pending.action === "clock-in") { if (runningQuery.data) { 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) { 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); try { await clockIn.mutateAsync({ clientId, description: resolveClockDescription(pending.title || DEFAULT_CLOCK_DESCRIPTION), rate: rate ?? undefined, }); await utils.timeEntries.getRunning.invalidate(); router.push("/(app)/timer"); } catch (err) { Alert.alert( "Clock in failed", err instanceof Error ? err.message : "Could not start the timer.", ); router.push("/(app)/timer"); } } })(); }, [ activeAccountId, clockIn, clockOut, clientsQuery.data, clientsQuery.isLoading, isLocked, runningQuery.data, runningQuery.isLoading, utils, ]); return null; }