From 06bc91ac131e638090f59397a085a424103a18d5 Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Mon, 22 Jun 2026 16:06:17 -0400 Subject: [PATCH] Redesign mobile time clock, add shortcuts, and improve account management. Add iOS Shortcuts/Siri intents, local send-reminder notifications, stable client picker with last-client defaults, account refresh/remove, and softer session handling on unauthorized API responses. Co-authored-by: Cursor --- README.md | 21 +- app.json | 22 +- app/(app)/_layout.tsx | 4 + app/(app)/index.tsx | 21 + app/(app)/invoices/[id].tsx | 48 ++ app/(app)/invoices/edit/[id].tsx | 73 +- app/(app)/settings.tsx | 125 ++- app/(app)/timer.tsx | 12 +- app/_layout.tsx | 6 +- bun.lock | 10 + components/AccountSwitcher.tsx | 91 +- components/InvoiceReminderSync.tsx | 65 ++ components/SessionSync.tsx | 27 + components/ShortcutHandler.tsx | 141 +++ components/invoices/LineItemEditor.tsx | 13 +- components/time-clock/TimeClockPanel.tsx | 854 ++++++++++++++----- contexts/AccountsContext.tsx | 41 +- contexts/AuthContext.tsx | 12 +- lib/account-actions.ts | 43 + lib/auth-storage.ts | 35 + lib/invoice-send-reminders.ts | 151 ++++ lib/query-client.ts | 25 + lib/shortcuts.ts | 53 ++ lib/time-clock-prefs.ts | 21 + lib/time-clock.ts | 14 + lib/trpc-errors.ts | 8 + lib/trpc.tsx | 39 +- package.json | 2 + plugins/app-intents/BeenVoiceShortcuts.swift | 36 + plugins/app-intents/ClockInIntent.swift | 33 + plugins/app-intents/ClockOutIntent.swift | 21 + plugins/app-intents/OpenTimerIntent.swift | 21 + plugins/withAppIntents.js | 76 ++ 33 files changed, 1844 insertions(+), 320 deletions(-) create mode 100644 components/InvoiceReminderSync.tsx create mode 100644 components/SessionSync.tsx create mode 100644 components/ShortcutHandler.tsx create mode 100644 lib/account-actions.ts create mode 100644 lib/invoice-send-reminders.ts create mode 100644 lib/query-client.ts create mode 100644 lib/shortcuts.ts create mode 100644 lib/time-clock-prefs.ts create mode 100644 lib/trpc-errors.ts create mode 100644 plugins/app-intents/BeenVoiceShortcuts.swift create mode 100644 plugins/app-intents/ClockInIntent.swift create mode 100644 plugins/app-intents/ClockOutIntent.swift create mode 100644 plugins/app-intents/OpenTimerIntent.swift create mode 100644 plugins/withAppIntents.js diff --git a/README.md b/README.md index 05d1ab6..6e9d76b 100644 --- a/README.md +++ b/README.md @@ -84,12 +84,25 @@ bun run ios Full flow: [docs/ARCHITECTURE.md#multi-account-model](./docs/ARCHITECTURE.md#multi-account-model) -## Deep links +## Deep links & Shortcuts -| Scheme | Screen | -|--------|--------| +| URL | Action | +|-----|--------| | `beenvoice://reset-password?token=…` | Reset password | -| `beenvoice://timer` | Timer tab (from Live Activity) | +| `beenvoice://timer` | Open time clock | +| `beenvoice://shortcuts/clock-in` | Clock in (last client) | +| `beenvoice://shortcuts/clock-in?title=…` | Clock in with title | +| `beenvoice://shortcuts/clock-out` | Clock out running timer | + +**iOS Shortcuts / Siri** (dev build after `npx expo prebuild`): + +- **Clock In** — starts the timer with your last client +- **Clock Out** — stops the running timer +- **Open Time Clock** — opens the timer tab + +Try “Hey Siri, clock in with beenvoice” or add actions from the Shortcuts app under beenvoice. + +Rebuild iOS after pulling shortcut changes: `npx expo prebuild --platform ios && bun run ios` ## Project layout diff --git a/app.json b/app.json index c7601e4..ef74f72 100644 --- a/app.json +++ b/app.json @@ -12,7 +12,8 @@ "bundleIdentifier": "com.beenvoice.app", "icon": "./assets/beenvoice.icon", "infoPlist": { - "NSFaceIDUsageDescription": "Unlock beenvoice with Face ID when returning to the app." + "NSFaceIDUsageDescription": "Unlock beenvoice with Face ID when returning to the app.", + "NSUserNotificationsUsageDescription": "beenvoice sends reminders when it's time to send an invoice." } }, "android": { @@ -35,6 +36,14 @@ }, "plugins": [ "expo-dev-client", + [ + "expo-build-properties", + { + "ios": { + "buildReactNativeFromSource": true + } + } + ], "expo-router", "expo-secure-store", [ @@ -58,7 +67,16 @@ "faceIDPermission": "Unlock beenvoice with Face ID when returning to the app." } ], - "@react-native-community/datetimepicker" + [ + "expo-notifications", + { + "icon": "./assets/images/icon.png", + "color": "#18181B", + "sounds": [] + } + ], + "@react-native-community/datetimepicker", + "./plugins/withAppIntents.js" ], "experiments": { "typedRoutes": true diff --git a/app/(app)/_layout.tsx b/app/(app)/_layout.tsx index d676301..b9d1182 100644 --- a/app/(app)/_layout.tsx +++ b/app/(app)/_layout.tsx @@ -2,6 +2,8 @@ import { Platform } from "react-native"; import { NativeTabs } from "expo-router/unstable-native-tabs"; import { AppLockOverlay } from "@/components/AppLockOverlay"; +import { InvoiceReminderSync } from "@/components/InvoiceReminderSync"; +import { ShortcutHandler } from "@/components/ShortcutHandler"; import { useAppTheme } from "@/contexts/ThemeContext"; import { AppLockProvider } from "@/contexts/AppLockContext"; @@ -71,6 +73,8 @@ export default function AppLayout() { Settings + + ); diff --git a/app/(app)/index.tsx b/app/(app)/index.tsx index bba5587..bdb64b8 100644 --- a/app/(app)/index.tsx +++ b/app/(app)/index.tsx @@ -62,6 +62,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 ?? []; return ( @@ -118,6 +119,26 @@ export default function DashboardScreen() { ) : null} + {sendReminderDue.length > 0 ? ( + + + + {sendReminderDue.length} draft{" "} + {sendReminderDue.length === 1 ? "invoice" : "invoices"} ready to send + + + {sendReminderDue + .slice(0, 2) + .map( + (inv) => + `${inv.invoicePrefix ?? "#"}${inv.invoiceNumber} (${inv.client?.name ?? "Client"})`, + ) + .join(" · ")} + + + + ) : null} +