From 57e1aa658a516925c5d6b9263eda91eaf753dc7c Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Mon, 29 Jun 2026 17:58:50 -0400 Subject: [PATCH] Stabilize mobile auth session handling --- app.json | 2 +- app/(app)/entities/index.tsx | 126 ++++++--- app/(app)/index.tsx | 39 ++- app/(app)/invoices/[id].tsx | 57 ++-- app/(app)/invoices/edit/[id].tsx | 10 + app/(app)/invoices/index.tsx | 120 +++++++-- app/(app)/invoices/new.tsx | 10 + app/(app)/invoices/send/[id].tsx | 11 +- app/(app)/settings.tsx | 11 +- app/(auth)/register.tsx | 7 +- app/(auth)/sign-in.tsx | 10 +- app/_layout.tsx | 29 +- components/AccountSwitcher.tsx | 3 +- components/SessionSync.tsx | 33 ++- components/invoices/LineItemEditor.tsx | 34 ++- components/time-clock/TimeClockPanel.tsx | 212 +++++++++++---- components/time-clock/TimeEntryEditSheet.tsx | 267 +++++++++++++++++++ lib/account-actions.ts | 17 +- lib/auth-api.ts | 14 +- lib/auth-cookie.ts | 55 ++++ lib/auth-session.ts | 60 +++++ lib/auth-storage.ts | 2 + lib/query-client.ts | 10 +- lib/trpc-errors.ts | 119 ++++++++- lib/trpc.tsx | 60 ++++- 25 files changed, 1126 insertions(+), 192 deletions(-) create mode 100644 components/time-clock/TimeEntryEditSheet.tsx create mode 100644 lib/auth-cookie.ts create mode 100644 lib/auth-session.ts diff --git a/app.json b/app.json index 41833f5..2322639 100644 --- a/app.json +++ b/app.json @@ -10,7 +10,7 @@ "ios": { "supportsTablet": true, "bundleIdentifier": "com.beenvoice.app", - "buildNumber": "13", + "buildNumber": "17", "icon": "./assets/beenvoice.icon", "infoPlist": { "ITSAppUsesNonExemptEncryption": false, diff --git a/app/(app)/entities/index.tsx b/app/(app)/entities/index.tsx index f79f95c..0778aba 100644 --- a/app/(app)/entities/index.tsx +++ b/app/(app)/entities/index.tsx @@ -1,6 +1,7 @@ import { router } from "expo-router"; import { useState } from "react"; import { + Alert, Pressable, RefreshControl, ScrollView, @@ -15,6 +16,7 @@ import { FloatingActionButton } from "@/components/FloatingActionButton"; import { GlassSurface } from "@/components/GlassSurface"; import { LoadingScreen } from "@/components/LoadingScreen"; import { PageHeader } from "@/components/PageHeader"; +import { SwipeableRow } from "@/components/SwipeableRow"; import { TabPage } from "@/components/TabPage"; import { TabScrollView } from "@/components/TabScrollView"; import { fonts, spacing } from "@/constants/theme"; @@ -38,6 +40,12 @@ export default function EntitiesScreen() { const clientsQuery = api.clients.getAll.useQuery(); const businessesQuery = api.businesses.getAll.useQuery(); + const deleteClient = api.clients.delete.useMutation({ + onSuccess: () => void clientsQuery.refetch(), + }); + const deleteBusiness = api.businesses.delete.useMutation({ + onSuccess: () => void businessesQuery.refetch(), + }); const activeQuery = tab === "clients" ? clientsQuery : businessesQuery; const isLoading = @@ -68,6 +76,20 @@ export default function EntitiesScreen() { else void businessesQuery.refetch(); } + function confirmDelete(id: string, name: string) { + Alert.alert(`Delete ${tab === "clients" ? "client" : "business"}?`, `Remove ${name}?`, [ + { text: "Cancel", style: "cancel" }, + { + text: "Delete", + style: "destructive", + onPress: () => { + if (tab === "clients") deleteClient.mutate({ id }); + else deleteBusiness.mutate({ id }); + }, + }, + ]); + } + return ( @@ -112,25 +134,45 @@ export default function EntitiesScreen() { ) : ( clients.map((client) => ( - router.push(`/(app)/entities/clients/${client.id}`)} + backgroundColor={colors.cardGlass} + actions={[ + { + key: "edit", + label: "Edit", + icon: "create-outline", + color: "#fff", + backgroundColor: colors.primary, + onPress: () => router.push(`/(app)/entities/clients/edit/${client.id}`), + }, + { + key: "delete", + label: "Delete", + icon: "trash-outline", + color: "#fff", + backgroundColor: colors.destructive, + onPress: () => confirmDelete(client.id, client.name), + }, + ]} > - - - {client.name} - {client.email ? ( - {client.email} - ) : null} - {client.defaultHourlyRate != null ? ( - - {formatCurrency(client.defaultHourlyRate, client.currency ?? "USD")} - /hr - - ) : null} - - - + router.push(`/(app)/entities/clients/${client.id}`)}> + + + {client.name} + {client.email ? ( + {client.email} + ) : null} + {client.defaultHourlyRate != null ? ( + + {formatCurrency(client.defaultHourlyRate, client.currency ?? "USD")} + /hr + + ) : null} + + + + )) ) ) : businesses.length === 0 ? ( @@ -142,25 +184,45 @@ export default function EntitiesScreen() { ) : ( businesses.map((business) => ( - router.push(`/(app)/entities/businesses/${business.id}`)} + backgroundColor={colors.cardGlass} + actions={[ + { + key: "edit", + label: "Edit", + icon: "create-outline", + color: "#fff", + backgroundColor: colors.primary, + onPress: () => router.push(`/(app)/entities/businesses/edit/${business.id}`), + }, + { + key: "delete", + label: "Delete", + icon: "trash-outline", + color: "#fff", + backgroundColor: colors.destructive, + onPress: () => confirmDelete(business.id, business.name), + }, + ]} > - - - - {business.name} - {business.isDefault ? ( - Default + router.push(`/(app)/entities/businesses/${business.id}`)}> + + + + {business.name} + {business.isDefault ? ( + Default + ) : null} + + {business.nickname ? ( + {business.nickname} ) : null} + {business.email ? {business.email} : null} - {business.nickname ? ( - {business.nickname} - ) : null} - {business.email ? {business.email} : null} - - - + + + )) )} diff --git a/app/(app)/index.tsx b/app/(app)/index.tsx index bdb64b8..36184a3 100644 --- a/app/(app)/index.tsx +++ b/app/(app)/index.tsx @@ -20,6 +20,7 @@ import { useThemedStyles } from "@/lib/use-themed-styles"; import { getInvoiceStatus } from "@/lib/invoice-status"; import { formatElapsedHoursMinutes, resolveClockDescription } from "@/lib/time-clock"; import { useRunningElapsed } from "@/lib/use-running-elapsed"; +import { formatTrpcErrorMessage } from "@/lib/trpc-errors"; import { api } from "@/lib/trpc"; export default function DashboardScreen() { @@ -41,7 +42,7 @@ export default function DashboardScreen() { Could not load dashboard - {statsQuery.error.message} + {formatTrpcErrorMessage(statsQuery.error)} @@ -62,7 +63,7 @@ export default function DashboardScreen() { : "No change vs last month"; const maxRevenue = Math.max(...stats.revenueChartData.map((d) => d.revenue), 1); - const sendReminderDue = stats.sendReminderDue ?? []; + const sendReminderDue = stats.recentInvoices.filter((inv) => inv.status === "draft"); return ( @@ -146,8 +147,31 @@ export default function DashboardScreen() { variant="secondary" onPress={() => router.push("/(app)/invoices")} /> +