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
+13 -17
View File
@@ -22,6 +22,7 @@ import { fonts, spacing } from "@/constants/theme";
import { useAppTheme } from "@/contexts/ThemeContext";
import { formatCurrency } from "@/lib/format";
import { getInvoiceStatus } from "@/lib/invoice-status";
import { validateLineItems } from "@/lib/form-validation";
import { useTabBarScrollPadding } from "@/lib/tab-bar-insets";
import type { ThemeColors } from "@/lib/theme-palette";
import { useThemedStyles } from "@/lib/use-themed-styles";
@@ -100,6 +101,8 @@ export default function InvoiceEditScreen() {
const taxAmount = subtotal * (taxRate / 100);
const total = subtotal + taxAmount;
const currency = invoice?.currency ?? "USD";
const lineItemsError = validateLineItems(items);
const canSave = !lineItemsError;
if (!id) {
return <LoadingScreen message="Invalid invoice" />;
@@ -170,6 +173,7 @@ export default function InvoiceEditScreen() {
}
function handleSave() {
if (!canSave) return;
setError(null);
const parsedItems: Array<{
@@ -180,25 +184,11 @@ export default function InvoiceEditScreen() {
}> = [];
for (const item of items) {
const hours = Number(item.hours);
const rate = Number(item.rate);
if (!item.description.trim()) {
setError("Each line needs a description");
return;
}
if (Number.isNaN(hours) || hours < 0) {
setError("Hours must be a valid number");
return;
}
if (Number.isNaN(rate) || rate < 0) {
setError("Rate must be a valid number");
return;
}
parsedItems.push({
date: item.date,
description: item.description.trim(),
hours,
rate,
hours: Number(item.hours),
rate: Number(item.rate),
});
}
@@ -274,10 +264,16 @@ export default function InvoiceEditScreen() {
</View>
</Card>
{lineItemsError ? <Text style={styles.error}>{lineItemsError}</Text> : null}
{error ? <Text style={styles.error}>{error}</Text> : null}
<View style={styles.actions}>
<Button title="Save changes" loading={updateInvoice.isPending} onPress={handleSave} />
<Button
title="Save changes"
loading={updateInvoice.isPending}
disabled={!canSave}
onPress={handleSave}
/>
{status !== "paid" ? (
<Button
title={status === "draft" ? "Send invoice" : "Resend invoice"}
+29 -34
View File
@@ -23,6 +23,11 @@ import { fonts, spacing } from "@/constants/theme";
import { useAppTheme } from "@/contexts/ThemeContext";
import { formatCurrency } from "@/lib/format";
import { defaultDueDate, generateInvoiceNumber } from "@/lib/invoice-number";
import {
isRequiredString,
isValidTaxRate,
validateLineItems,
} from "@/lib/form-validation";
import { useTabBarScrollPadding } from "@/lib/tab-bar-insets";
import type { ThemeColors } from "@/lib/theme-palette";
import { useThemedStyles } from "@/lib/use-themed-styles";
@@ -104,6 +109,19 @@ export default function NewInvoiceScreen() {
const taxAmount = subtotal * (parsedTaxRate / 100);
const total = subtotal + taxAmount;
const clientError = clientId ? undefined : "Select a client";
const invoiceNumberError = isRequiredString(invoiceNumber)
? undefined
: "Invoice number is required";
const taxError = isValidTaxRate(taxRate) ? undefined : "Tax rate must be between 0 and 100";
const lineItemsError = validateLineItems(items);
const canCreate =
clientOptions.length > 0 &&
!clientError &&
!invoiceNumberError &&
!taxError &&
!lineItemsError;
if (clientsQuery.isLoading) {
return <LoadingScreen message="Loading…" />;
}
@@ -140,18 +158,9 @@ export default function NewInvoiceScreen() {
}
function handleCreate() {
if (!canCreate) return;
setError(null);
if (!clientId) {
setError("Select a client");
return;
}
if (!invoiceNumber.trim()) {
setError("Invoice number is required");
return;
}
const parsedItems: Array<{
date: Date;
description: string;
@@ -160,41 +169,21 @@ export default function NewInvoiceScreen() {
}> = [];
for (const item of items) {
const hours = Number(item.hours);
const rate = Number(item.rate);
if (!item.description.trim()) {
setError("Each line needs a description");
return;
}
if (Number.isNaN(hours) || hours < 0) {
setError("Hours must be a valid number");
return;
}
if (Number.isNaN(rate) || rate < 0) {
setError("Rate must be a valid number");
return;
}
parsedItems.push({
date: item.date,
description: item.description.trim(),
hours,
rate,
hours: Number(item.hours),
rate: Number(item.rate),
});
}
const tax = Number(taxRate);
if (Number.isNaN(tax) || tax < 0 || tax > 100) {
setError("Tax rate must be between 0 and 100");
return;
}
createInvoice.mutate({
clientId,
invoiceNumber: invoiceNumber.trim(),
issueDate,
dueDate,
notes,
taxRate: tax,
taxRate: Number(taxRate),
currency,
items: parsedItems,
status: "draft",
@@ -232,6 +221,8 @@ export default function NewInvoiceScreen() {
placeholder="Select client…"
value={clientId}
options={clientOptions}
required
error={clientError}
onValueChange={setClientId}
/>
)}
@@ -240,6 +231,8 @@ export default function NewInvoiceScreen() {
value={invoiceNumber}
onChangeText={setInvoiceNumber}
autoCapitalize="characters"
required
error={invoiceNumberError}
/>
<DateTimeField
label="Issue date"
@@ -256,6 +249,7 @@ export default function NewInvoiceScreen() {
value={taxRate}
onChangeText={setTaxRate}
keyboardType="decimal-pad"
error={taxError}
/>
<Input
label="Notes"
@@ -298,12 +292,13 @@ export default function NewInvoiceScreen() {
</View>
</Card>
{lineItemsError ? <Text style={styles.error}>{lineItemsError}</Text> : null}
{error ? <Text style={styles.error}>{error}</Text> : null}
<Button
title="Create invoice"
loading={createInvoice.isPending}
disabled={clientOptions.length === 0}
disabled={!canCreate}
onPress={handleCreate}
/>
</ScrollView>