Files
beenvoice-app/components/ShortcutHandler.tsx
T
soconnor 355b14faef Add local iOS release pipeline, fix shortcuts, and improve invoice UX.
Enable App Store builds without EAS, iOS 18 App Intents plugins, and signing
fixes for distribution export. Add mobile invoice PDF preview, compact line
items, and more reliable shortcut deep-link handling.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 01:08:20 -04:00

155 lines
4.6 KiB
TypeScript

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<ParsedShortcut | null>(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;
}