Files
beenvoice-app/lib/time-clock-live-activity.ts
T
soconnor 32ffe782ea Fix Live Activity lock screen rendering and polish multi-account auth.
Flatten widget layouts and use system colors so banner and expanded regions render on vibrant lock screens; migrate auth sessions per account to prevent double sign-in; scope app lock PIN to accounts; default clock description to "Clock In"; add architecture docs and deferred form validation on auth screens.

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

114 lines
3.0 KiB
TypeScript

import { requireOptionalNativeModule } from "expo-modules-core";
import { Platform } from "react-native";
import { formatElapsedHoursMinutes, formatElapsedSeconds, resolveClockDescription } from "@/lib/time-clock";
import type { TimeClockActivityProps } from "@/lib/time-clock-live-activity.types";
type RunningEntry = {
description: string;
startedAt: Date | 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 {
startedAtMs: new Date(running.startedAt).getTime(),
elapsed: formatElapsedSeconds(elapsedSeconds),
elapsedShort: formatElapsedHoursMinutes(elapsedSeconds),
clockTime: new Date().toLocaleTimeString(undefined, {
hour: "numeric",
minute: "2-digit",
}),
description: resolveClockDescription(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 (error) {
if (__DEV__) {
console.warn("[LiveActivity] sync failed:", error);
}
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;
}
}