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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user