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>
This commit is contained in:
2026-06-18 01:23:36 -04:00
parent e6ea3d7c5d
commit 32ffe782ea
35 changed files with 1659 additions and 442 deletions
+36 -13
View File
@@ -14,13 +14,14 @@ import { useAppTheme } from "@/contexts/ThemeContext";
import { useTabBarScrollPadding } from "@/lib/tab-bar-insets";
import { tabLayout } from "@/lib/tab-layout";
import { formatDateTime } from "@/lib/format";
import { parseNonNegativeNumber } from "@/lib/form-validation";
import type { ThemeColors } from "@/lib/theme-palette";
import { useThemedStyles } from "@/lib/use-themed-styles";
import {
endTimeClockLiveActivity,
syncTimeClockLiveActivity,
} from "@/lib/time-clock-live-activity";
import { describeClockOutOutcome, formatElapsedSeconds } from "@/lib/time-clock";
import { DEFAULT_CLOCK_DESCRIPTION, describeClockOutOutcome, formatElapsedSeconds, resolveClockDescription } from "@/lib/time-clock";
import { useRunningElapsed } from "@/lib/use-running-elapsed";
import { api } from "@/lib/trpc";
@@ -48,7 +49,7 @@ export function TimeClockPanel({
const [clientId, setClientId] = useState(defaultClientId);
const [invoiceId, setInvoiceId] = useState(defaultInvoiceId);
const [description, setDescription] = useState("");
const [description, setDescription] = useState(DEFAULT_CLOCK_DESCRIPTION);
const [rateText, setRateText] = useState("");
const [startedAt, setStartedAt] = useState(() => new Date());
@@ -109,7 +110,7 @@ export function TimeClockPanel({
utils.invoices.getBillable.invalidate(),
utils.dashboard.getStats.invalidate(),
]);
setDescription("");
setDescription(DEFAULT_CLOCK_DESCRIPTION);
},
});
@@ -117,7 +118,7 @@ export function TimeClockPanel({
if (!running) return;
setClientId(running.clientId ?? "");
setInvoiceId(running.invoiceId ?? "");
setDescription(running.description);
setDescription(running.description?.trim() || DEFAULT_CLOCK_DESCRIPTION);
setRateText(running.rate != null ? String(running.rate) : "");
}, [running]);
@@ -146,7 +147,7 @@ export function TimeClockPanel({
};
sync();
const interval = setInterval(sync, 30_000);
const interval = setInterval(sync, 15_000);
return () => clearInterval(interval);
}, [running, description]);
@@ -169,12 +170,24 @@ export function TimeClockPanel({
[billableInvoices],
);
const clockInErrors = useMemo(() => {
const next: { clientId?: string; rate?: string } = {};
if (!clientId) next.clientId = "Select a client";
if (rateText.trim() && parseNonNegativeNumber(rateText) === null) {
next.rate = "Enter a valid hourly rate";
}
return next;
}, [clientId, rateText]);
const canClockIn = Object.keys(clockInErrors).length === 0;
async function handleClockIn() {
if (!canClockIn) return;
try {
const backdated =
Math.abs(Date.now() - startedAt.getTime()) > 60_000 ? startedAt : undefined;
await clockIn.mutateAsync({
description: description.trim(),
description: resolveClockDescription(description),
clientId: clientId || "",
invoiceId: invoiceId || undefined,
rate: rate || undefined,
@@ -188,7 +201,9 @@ export function TimeClockPanel({
async function handleClockOut() {
try {
await clockOut.mutateAsync({ description: description.trim() || undefined });
await clockOut.mutateAsync({
description: description.trim() ? description.trim() : undefined,
});
} catch (err) {
Alert.alert("Clock out failed", err instanceof Error ? err.message : "Try again");
}
@@ -268,7 +283,7 @@ export function TimeClockPanel({
</View>
<Text style={styles.timerValue}>{formatElapsedSeconds(elapsed)}</Text>
<Text style={styles.runningTitle}>
{description.trim() || "No description"}
{resolveClockDescription(description)}
</Text>
<Text style={styles.runningMeta}>
Started {formatDateTime(running.startedAt)}
@@ -307,7 +322,7 @@ export function TimeClockPanel({
label="Description"
value={description}
onChangeText={setDescription}
placeholder="What are you working on?"
placeholder={DEFAULT_CLOCK_DESCRIPTION}
/>
</View>
</Card>
@@ -319,6 +334,8 @@ export function TimeClockPanel({
placeholder="Select client…"
value={clientId}
options={clientOptions}
required
error={clockInErrors.clientId}
onValueChange={(next) => {
setClientId(next);
setInvoiceId("");
@@ -342,7 +359,7 @@ export function TimeClockPanel({
label="Description"
value={description}
onChangeText={setDescription}
placeholder="What are you working on?"
placeholder={DEFAULT_CLOCK_DESCRIPTION}
/>
<Input
@@ -350,7 +367,8 @@ export function TimeClockPanel({
value={rateText}
onChangeText={setRateText}
keyboardType="decimal-pad"
placeholder="0.00"
placeholder="Optional"
error={clockInErrors.rate}
/>
<DateTimeField
@@ -374,7 +392,12 @@ export function TimeClockPanel({
onPress={handleClockOut}
/>
) : (
<Button title="Clock in" loading={clockIn.isPending} onPress={handleClockIn} />
<Button
title="Clock in"
loading={clockIn.isPending}
disabled={!canClockIn}
onPress={handleClockIn}
/>
)}
{todayEntries.length > 0 ? (
@@ -387,7 +410,7 @@ export function TimeClockPanel({
const row = (
<>
<View style={styles.entryMeta}>
<Text style={styles.entryTitle}>{entry.description || "No description"}</Text>
<Text style={styles.entryTitle}>{resolveClockDescription(entry.description)}</Text>
<Text style={styles.entrySub}>
{entry.client?.name ?? "No client"}
{invoiceLabel ? ` · ${invoiceLabel}` : " · not billed"}