Add beenvoice mobile companion app with full dark mode support.

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>
This commit is contained in:
2026-06-17 22:36:37 -04:00
parent 8a7a8df477
commit 14c880123c
93 changed files with 8849 additions and 7849 deletions
+110
View File
@@ -0,0 +1,110 @@
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;
}
}