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