diff --git a/.env.example b/.env.example index 671d4e4..d84b729 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ # beenvoice API base URL (no trailing slash) -# Omit or leave unset in production builds — app defaults to https://beenvoice.soconnor.dev +# Omit or leave unset in production builds — app defaults to https://beenvoice.app # Local dev on physical iPhone: use your Mac's LAN IP, e.g. http://192.168.1.42:3000 EXPO_PUBLIC_API_URL=http://localhost:3000 diff --git a/.ios-release.env.example b/.ios-release.env.example index aabdcf2..b0459ce 100644 --- a/.ios-release.env.example +++ b/.ios-release.env.example @@ -12,7 +12,7 @@ APPLE_TEAM_ID= # then re-run the full release (not --export-only). # Production API baked into the JS bundle (App Store / TestFlight) -EXPO_PUBLIC_API_URL=https://beenvoice.soconnor.dev +EXPO_PUBLIC_API_URL=https://beenvoice.app # App Store Connect API key (Users and Access → Integrations → App Store Connect API) # Create a key with Developer role. Download the .p8 once — Apple won't show it again. diff --git a/AGENTS.md b/AGENTS.md index 9e0c4ad..a3e4d56 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ Expo SDK **56**. Read [Expo v56 docs](https://docs.expo.dev/versions/v56.0.0/) b ## Conventions - **Package manager**: Bun only -- **API types**: import `AppRouter` from `beenvoice/server/api/root` (tsconfig path `../beenvoice/src/*`) +- **API types**: import `AppRouter` from `beenvoice/server/api/root` (tsconfig path `../beenvoice-web/src/*`) - **Styling**: `useAppTheme()` + `useThemedStyles()`; tokens in `lib/theme-palette.ts` - **Forms**: `lib/form-validation.ts`; show errors only after blur/submit (`useFieldVisibility`) - **Auth**: never remount account without migrating SecureStore session (`lib/auth-storage.ts`) @@ -30,4 +30,4 @@ Expo SDK **56**. Read [Expo v56 docs](https://docs.expo.dev/versions/v56.0.0/) b ## Server repo -Sibling `../beenvoice` — run `bun run dev` on :3000 before mobile dev. +Sibling `../beenvoice-web` — run `bun run dev` on :3000 before mobile dev. diff --git a/README.md b/README.md index d7d3168..19f6434 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # beenvoice Mobile -Expo companion for [beenvoice](../beenvoice) — dashboard, time clock, invoices, clients, businesses, and settings. Shares the **same tRPC API** and **better-auth** sessions as the web app. +Expo companion for [beenvoice-web](../beenvoice-web) — dashboard, time clock, invoices, clients, businesses, and settings. Shares the **same tRPC API** and **better-auth** sessions as the web app. **Architecture (dense):** [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) ## Prerequisites - [Bun](https://bun.sh) 1.3+ -- beenvoice API running ([setup](../beenvoice/README.md)) +- beenvoice API running ([setup](../beenvoice-web/README.md)) - Xcode + iOS Simulator (or device) for native dev build - **Not Expo Go** — widgets, SecureStore auth, and biometrics need `expo-dev-client` @@ -29,7 +29,7 @@ EXPO_PUBLIC_API_URL=http://localhost:3000 EXPO_PUBLIC_API_URL=http://192.168.1.42:3000 ``` -Omit `EXPO_PUBLIC_API_URL` in production builds to default to `https://beenvoice.soconnor.dev`. +Omit `EXPO_PUBLIC_API_URL` in production builds to default to `https://beenvoice.app`. Server must enable `@better-auth/expo` in `beenvoice/src/lib/auth.ts` with `beenvoice://` in `trustedOrigins`. @@ -37,7 +37,7 @@ Server must enable `@better-auth/expo` in `beenvoice/src/lib/auth.ts` with `been ```bash # Terminal 1 — API -cd ../beenvoice && bun run dev +cd ../beenvoice-web && bun run dev # Terminal 2 — mobile (builds native app if needed) cd beenvoice-app && bun run ios @@ -80,7 +80,7 @@ bun run ios - **Guest** auth storage: `beenvoice:guest` until first successful login - **Per account**: `beenvoice:auth:{host::userId}` in SecureStore - After login, `finalizeAuthenticatedAccount()` migrates session keys before activating the account (avoids double login) -- **Server picker**: Official (`beenvoice.soconnor.dev`) or custom URL on auth screens +- **Server picker**: Official (`beenvoice.app`) or custom URL on auth screens Full flow: [docs/ARCHITECTURE.md#multi-account-model](./docs/ARCHITECTURE.md#multi-account-model) @@ -94,16 +94,23 @@ Full flow: [docs/ARCHITECTURE.md#multi-account-model](./docs/ARCHITECTURE.md#mul | `beenvoice://shortcuts/clock-in?title=…` | Clock in with title | | `beenvoice://shortcuts/clock-out` | Clock out running timer | -**iOS Shortcuts / Siri** (requires native rebuild: `bunx expo prebuild --platform ios && bun run ios`): +**iOS Shortcuts / Siri** (requires a **native dev client or TestFlight build** — not Expo Go; iOS **18+**): - **Clock In** — starts the timer with your last client - **Clock Out** — stops the running timer - **Open Time Clock** — opens the timer tab -1. Install a fresh build on a physical iPhone (iOS 16+). -2. Open the app once while signed in (registers shortcuts with the system). -3. Shortcuts app → search **beenvoice** → add actions, or say “Hey Siri, clock in with beenvoice”. -4. Pick a client once on the Timer tab before the first clock-in shortcut. +Shortcuts are **not pre-installed** in your Shortcuts library. To add one: + +1. Install a fresh native build (`bunx expo prebuild --platform ios && bun run ios`, or a new EAS/TestFlight build). +2. Open beenvoice once while signed in. +3. Open **Shortcuts** → **+** → **Add Action** → search **beenvoice** (or “Clock In”). +4. Choose **Clock In**, **Clock Out**, or **Open Time Clock**. +5. Pick a client once on the Timer tab before the first clock-in shortcut. + +You can also say “Hey Siri, clock in with beenvoice”. Settings → Shortcuts & Siri in the app has a setup guide and test links. + +If beenvoice actions never appear when searching in Shortcuts, the installed build predates App Intents — rebuild and reinstall. **Test deep links:** @@ -138,6 +145,6 @@ widgets/ # iOS Live Activity (TimeClockActivity) ## Related -- [beenvoice README](../beenvoice/README.md) -- [beenvoice ARCHITECTURE](../beenvoice/docs/ARCHITECTURE.md) +- [beenvoice-web README](../beenvoice-web/README.md) +- [beenvoice-web ARCHITECTURE](../beenvoice-web/docs/ARCHITECTURE.md) - [Workspace root README](../README.md) diff --git a/app.json b/app.json index 52ac725..33bada8 100644 --- a/app.json +++ b/app.json @@ -10,7 +10,7 @@ "ios": { "supportsTablet": true, "bundleIdentifier": "com.beenvoice.app", - "buildNumber": "7", + "buildNumber": "11", "icon": "./assets/beenvoice.icon", "infoPlist": { "ITSAppUsesNonExemptEncryption": false, diff --git a/app/(app)/_layout.tsx b/app/(app)/_layout.tsx index b9d1182..2aed1e9 100644 --- a/app/(app)/_layout.tsx +++ b/app/(app)/_layout.tsx @@ -4,6 +4,7 @@ import { NativeTabs } from "expo-router/unstable-native-tabs"; import { AppLockOverlay } from "@/components/AppLockOverlay"; import { InvoiceReminderSync } from "@/components/InvoiceReminderSync"; import { ShortcutHandler } from "@/components/ShortcutHandler"; +import { TimeClockLiveActivitySync } from "@/components/time-clock/TimeClockLiveActivitySync"; import { useAppTheme } from "@/contexts/ThemeContext"; import { AppLockProvider } from "@/contexts/AppLockContext"; @@ -74,6 +75,7 @@ export default function AppLayout() { + diff --git a/app/(app)/invoices/[id].tsx b/app/(app)/invoices/[id].tsx index 162fb2d..decd464 100644 --- a/app/(app)/invoices/[id].tsx +++ b/app/(app)/invoices/[id].tsx @@ -1,14 +1,12 @@ import { router, Stack, useLocalSearchParams } from "expo-router"; import { useMemo, useState } from "react"; -import { Alert, Platform, Pressable, ScrollView, StyleSheet, Text, View } from "react-native"; +import { Alert, Platform, ScrollView, StyleSheet, Text, View } from "react-native"; import { AppBackground } from "@/components/AppBackground"; -import { - InvoiceEditorSectionTabs, - type InvoiceEditorSection, -} from "@/components/invoices/InvoiceEditorSectionTabs"; +import { InvoiceViewChips, type InvoiceViewSection } from "@/components/invoices/InvoiceViewChips"; import { InvoicePdfPreview } from "@/components/invoices/InvoicePdfPreview"; import { InvoiceTotals } from "@/components/invoices/InvoiceTotals"; +import { InvoiceDetailActions } from "@/components/invoices/InvoiceDetailActions"; import { LoadingScreen } from "@/components/LoadingScreen"; import { StatusBadge } from "@/components/StatusBadge"; import { Button } from "@/components/ui/Button"; @@ -24,12 +22,11 @@ import { useTabBarScrollPadding } from "@/lib/tab-bar-insets"; import { api } from "@/lib/trpc"; export default function InvoiceDetailScreen() { - const { colors } = useAppTheme(); const styles = useThemedStyles(createInvoiceDetailStyles); const { id } = useLocalSearchParams<{ id: string }>(); const utils = api.useUtils(); const scrollPadding = useTabBarScrollPadding(); - const [section, setSection] = useState("edit"); + const [section, setSection] = useState("details"); const invoiceQuery = api.invoices.getById.useQuery( { id: id ?? "" }, @@ -45,16 +42,6 @@ export default function InvoiceDetailScreen() { onError: (err) => Alert.alert("Update failed", err.message), }); - const sendInvoice = api.email.sendInvoice.useMutation({ - onSuccess: (data) => { - Alert.alert("Invoice sent", data.message); - void utils.invoices.getById.invalidate({ id: id ?? "" }); - void utils.invoices.getAll.invalidate(); - void utils.dashboard.getStats.invalidate(); - }, - onError: (err) => Alert.alert("Could not send invoice", err.message), - }); - const sendPaymentReminder = api.invoices.sendReminder.useMutation({ onSuccess: () => { Alert.alert("Reminder sent", "Payment reminder emailed to the client."); @@ -63,6 +50,12 @@ export default function InvoiceDetailScreen() { onError: (err) => Alert.alert("Could not send reminder", err.message), }); + const invoice = invoiceQuery.data; + const previewInput = useMemo( + () => (invoice ? buildPreviewPdfInputFromInvoice(invoice) : null), + [invoice], + ); + if (!id) { return ; } @@ -85,17 +78,12 @@ export default function InvoiceDetailScreen() { ); } - const invoice = invoiceQuery.data; const status = getInvoiceStatus(invoice); const subtotal = invoice.items.reduce((sum, item) => sum + item.amount, 0); const taxAmount = subtotal * (invoice.taxRate / 100); const clientEmail = invoice.client?.email?.trim() ?? ""; - const previewInput = useMemo( - () => buildPreviewPdfInputFromInvoice(invoice), - [invoice], - ); - function promptSendInvoice() { + function openSendScreen() { if (!clientEmail) { Alert.alert( "No client email", @@ -103,18 +91,14 @@ export default function InvoiceDetailScreen() { ); return; } - - Alert.alert( - status === "draft" ? "Send invoice" : "Resend invoice", - `Email this invoice to ${clientEmail}?`, - [ - { text: "Cancel", style: "cancel" }, - { - text: "Send", - onPress: () => sendInvoice.mutate({ invoiceId: invoice.id }), - }, - ], - ); + if (invoice.items.length === 0) { + Alert.alert( + "No line items", + "Add line items or clock time to this invoice before sending.", + ); + return; + } + router.push(`/(app)/invoices/send/${invoice.id}`); } function promptPaymentReminder() { @@ -159,25 +143,7 @@ export default function InvoiceDetailScreen() { return ( - - status !== "paid" ? ( - pressed && styles.headerPressed} - > - - {status === "draft" ? "Send" : "Resend"} - - - ) : null, - }} - /> + - router.push(`/(app)/invoices/edit/${invoice.id}`)} + onSend={openSendScreen} /> {section === "preview" ? ( @@ -215,6 +182,8 @@ export default function InvoiceDetailScreen() { ) : ( <> + + @@ -234,20 +203,27 @@ export default function InvoiceDetailScreen() { - {invoice.items.map((item) => ( - - - {item.description} - - {formatDate(item.date)} · {item.hours}h ×{" "} - {formatCurrency(item.rate, invoice.currency)} + {invoice.items.length === 0 ? ( + + No line items yet. Clock time to this invoice from the Timer tab, or edit to + add lines manually. + + ) : ( + invoice.items.map((item) => ( + + + {item.description} + + {formatDate(item.date)} · {item.hours}h ×{" "} + {formatCurrency(item.rate, invoice.currency)} + + + + {formatCurrency(item.amount, invoice.currency)} - - {formatCurrency(item.amount, invoice.currency)} - - - ))} + )) + )} 0 ? `Tax (${invoice.taxRate}%)` : undefined} @@ -264,43 +240,19 @@ export default function InvoiceDetailScreen() { ) : null} - - {status !== "paid" ? ( -