14c880123c
Expo app with dashboard, time clock, invoices, and settings — native tabs, glass UI, theme-aware components, and iOS Live Activities. Co-authored-by: Cursor <cursoragent@cursor.com>
111 lines
2.8 KiB
TypeScript
111 lines
2.8 KiB
TypeScript
import { requireOptionalNativeModule } from "expo-modules-core";
|
|
import { Platform } from "react-native";
|
|
|
|
import { formatElapsedHoursMinutes, formatElapsedSeconds } from "@/lib/time-clock";
|
|
import type { TimeClockActivityProps } from "@/lib/time-clock-live-activity.types";
|
|
|
|
type RunningEntry = {
|
|
description: string;
|
|
client?: { name: string } | null;
|
|
invoice?: { invoicePrefix: string | null; invoiceNumber: string } | null;
|
|
};
|
|
|
|
type LiveActivityHandle = {
|
|
update: (props: TimeClockActivityProps) => Promise<void>;
|
|
end: (policy?: "default" | "immediate") => Promise<void>;
|
|
};
|
|
|
|
type LiveActivityFactory = {
|
|
start: (props: TimeClockActivityProps, url?: string) => LiveActivityHandle;
|
|
getInstances: () => LiveActivityHandle[];
|
|
};
|
|
|
|
let factoryCache: LiveActivityFactory | null | undefined;
|
|
|
|
function isExpoWidgetsAvailable() {
|
|
return Platform.OS === "ios" && requireOptionalNativeModule("ExpoWidgets") != null;
|
|
}
|
|
|
|
function getFactory(): LiveActivityFactory | null {
|
|
if (factoryCache !== undefined) {
|
|
return factoryCache;
|
|
}
|
|
|
|
if (!isExpoWidgetsAvailable()) {
|
|
factoryCache = null;
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
factoryCache = require("@/widgets/TimeClockActivity").default as LiveActivityFactory;
|
|
} catch {
|
|
factoryCache = null;
|
|
}
|
|
|
|
return factoryCache;
|
|
}
|
|
|
|
export function isTimeClockLiveActivitySupported() {
|
|
return getFactory() != null;
|
|
}
|
|
|
|
export function buildTimeClockActivityProps(
|
|
running: RunningEntry,
|
|
elapsedSeconds: number,
|
|
): TimeClockActivityProps {
|
|
const invoice = running.invoice;
|
|
return {
|
|
elapsed: formatElapsedSeconds(elapsedSeconds),
|
|
elapsedShort: formatElapsedHoursMinutes(elapsedSeconds),
|
|
clockTime: new Date().toLocaleTimeString(undefined, {
|
|
hour: "numeric",
|
|
minute: "2-digit",
|
|
}),
|
|
description: running.description,
|
|
clientName: running.client?.name ?? "",
|
|
invoiceLabel: invoice
|
|
? `${invoice.invoicePrefix ?? "#"}${invoice.invoiceNumber}`
|
|
: "",
|
|
};
|
|
}
|
|
|
|
export async function syncTimeClockLiveActivity(
|
|
running: RunningEntry | null | undefined,
|
|
elapsedSeconds: number,
|
|
) {
|
|
const factory = getFactory();
|
|
if (!factory) return;
|
|
|
|
if (!running) {
|
|
await endTimeClockLiveActivity();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const props = buildTimeClockActivityProps(running, elapsedSeconds);
|
|
const instances = factory.getInstances();
|
|
|
|
if (instances.length > 0) {
|
|
await instances[0]!.update(props);
|
|
return;
|
|
}
|
|
|
|
factory.start(props, "beenvoice://timer");
|
|
} catch {
|
|
// Native module can disappear between checks (e.g. hot reload in Expo Go).
|
|
factoryCache = undefined;
|
|
}
|
|
}
|
|
|
|
export async function endTimeClockLiveActivity() {
|
|
const factory = getFactory();
|
|
if (!factory) return;
|
|
|
|
try {
|
|
const instances = factory.getInstances();
|
|
await Promise.all(instances.map((instance) => instance.end("immediate")));
|
|
} catch {
|
|
factoryCache = undefined;
|
|
}
|
|
}
|