From 6d2711e36e1a326ce1fb1b116662c74869366a0e Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Wed, 17 Jun 2026 23:14:58 -0400 Subject: [PATCH] Polish mobile app for App Store review and expand CRUD. Default to beenvoice.soconnor.dev with server settings hidden behind Advanced; add Entities tab with clients/businesses, invoice creation, UI fixes for dashboard layout, date fields, FAB position, and card-matched button radius. Co-authored-by: Cursor --- app/(app)/_layout.tsx | 34 +- app/(app)/entities/_layout.tsx | 76 ++++ app/(app)/entities/businesses/[id].tsx | 186 +++++++++ app/(app)/entities/businesses/edit/[id].tsx | 32 ++ app/(app)/entities/businesses/new.tsx | 25 ++ app/(app)/entities/clients/[id].tsx | 218 +++++++++++ app/(app)/entities/clients/edit/[id].tsx | 32 ++ app/(app)/entities/clients/new.tsx | 25 ++ app/(app)/entities/index.tsx | 255 ++++++++++++ app/(app)/index.tsx | 65 ++-- app/(app)/invoices/[id].tsx | 30 +- app/(app)/invoices/_layout.tsx | 24 +- app/(app)/invoices/edit/[id].tsx | 182 +++++---- app/(app)/invoices/index.tsx | 7 +- app/(app)/invoices/new.tsx | 411 ++++++++++++++++++++ app/(app)/settings.tsx | 50 ++- app/(auth)/forgot-password.tsx | 2 - app/(auth)/register.tsx | 2 - app/(auth)/sign-in.tsx | 2 - assets/beenvoice.icon/Assets/beenvoice.svg | 18 +- assets/images/android-icon-foreground.png | Bin 15041 -> 33197 bytes assets/images/android-icon-monochrome.png | Bin 15041 -> 33197 bytes assets/images/beenvoice-mark.svg | 15 + assets/images/favicon.png | Bin 1535 -> 1339 bytes assets/images/icon.png | Bin 38944 -> 33197 bytes assets/images/splash-icon.png | Bin 17949 -> 15195 bytes components/FloatingActionButton.tsx | 63 +++ components/Logo.tsx | 40 +- components/StatCard.tsx | 3 +- components/businesses/BusinessForm.tsx | 335 ++++++++++++++++ components/clients/ClientForm.tsx | 288 ++++++++++++++ components/invoices/LineItemEditor.tsx | 4 +- components/ui/Button.tsx | 2 +- components/ui/Card.tsx | 1 + components/ui/DateTimeField.tsx | 6 +- components/ui/StepperInput.tsx | 106 +++++ contexts/AccountsContext.tsx | 4 +- lib/config.ts | 7 +- lib/invoice-number.ts | 11 + lib/tab-bar-insets.ts | 22 +- lib/theme-palette.ts | 8 + 41 files changed, 2410 insertions(+), 181 deletions(-) create mode 100644 app/(app)/entities/_layout.tsx create mode 100644 app/(app)/entities/businesses/[id].tsx create mode 100644 app/(app)/entities/businesses/edit/[id].tsx create mode 100644 app/(app)/entities/businesses/new.tsx create mode 100644 app/(app)/entities/clients/[id].tsx create mode 100644 app/(app)/entities/clients/edit/[id].tsx create mode 100644 app/(app)/entities/clients/new.tsx create mode 100644 app/(app)/entities/index.tsx create mode 100644 app/(app)/invoices/new.tsx create mode 100644 assets/images/beenvoice-mark.svg create mode 100644 components/FloatingActionButton.tsx create mode 100644 components/businesses/BusinessForm.tsx create mode 100644 components/clients/ClientForm.tsx create mode 100644 components/ui/StepperInput.tsx create mode 100644 lib/invoice-number.ts diff --git a/app/(app)/_layout.tsx b/app/(app)/_layout.tsx index 5df1f81..a49528b 100644 --- a/app/(app)/_layout.tsx +++ b/app/(app)/_layout.tsx @@ -1,25 +1,22 @@ -import { DynamicColorIOS, Platform } from "react-native"; +import { Platform } from "react-native"; import { NativeTabs } from "expo-router/unstable-native-tabs"; import { AppLockOverlay } from "@/components/AppLockOverlay"; import { useAppTheme } from "@/contexts/ThemeContext"; -import { mutedForeground, primary } from "@/lib/beenvoice-theme"; import { AppLockProvider } from "@/contexts/AppLockContext"; export default function AppLayout() { - const { colors } = useAppTheme(); + const { colors, isDark } = useAppTheme(); - const tintColor = + const tintColor = colors.primary; + const labelColor = colors.mutedForeground; + const tabContentStyle = { backgroundColor: colors.background }; + const tabBarBlur = Platform.OS === "ios" - ? DynamicColorIOS({ light: primary, dark: "#FAFAFA" }) - : colors.primary; - - const labelColor = - Platform.OS === "ios" - ? DynamicColorIOS({ light: mutedForeground, dark: "#A1A1AA" }) - : colors.mutedForeground; - - const tabContentStyle = { backgroundColor: "transparent" as const }; + ? isDark + ? "systemChromeMaterialDark" + : "systemChromeMaterialLight" + : undefined; return ( @@ -30,6 +27,9 @@ export default function AppLayout() { selected: tintColor, }} labelStyle={{ color: labelColor }} + blurEffect={tabBarBlur} + disableTransparentOnScrollEdge + backgroundColor={Platform.OS === "android" ? colors.background : undefined} > Timer + + + Entities + + + + + + + + + + + ); +} diff --git a/app/(app)/entities/businesses/[id].tsx b/app/(app)/entities/businesses/[id].tsx new file mode 100644 index 0000000..04f0755 --- /dev/null +++ b/app/(app)/entities/businesses/[id].tsx @@ -0,0 +1,186 @@ +import { router, Stack, useLocalSearchParams } from "expo-router"; +import { Alert, ScrollView, StyleSheet, Text, View } from "react-native"; + +import { AppBackground } from "@/components/AppBackground"; +import { LoadingScreen } from "@/components/LoadingScreen"; +import { Button } from "@/components/ui/Button"; +import { Card } from "@/components/ui/Card"; +import { fonts, spacing } from "@/constants/theme"; +import { useAppTheme } from "@/contexts/ThemeContext"; +import { useTabBarScrollPadding } from "@/lib/tab-bar-insets"; +import type { ThemeColors } from "@/lib/theme-palette"; +import { useThemedStyles } from "@/lib/use-themed-styles"; +import { api } from "@/lib/trpc"; + +export default function BusinessDetailScreen() { + const { colors } = useAppTheme(); + const styles = useThemedStyles(createBusinessDetailStyles); + const { id } = useLocalSearchParams<{ id: string }>(); + const scrollPadding = useTabBarScrollPadding(); + const utils = api.useUtils(); + + const businessQuery = api.businesses.getById.useQuery( + { id: id ?? "" }, + { enabled: Boolean(id) }, + ); + + const setDefault = api.businesses.setDefault.useMutation({ + onSuccess: () => { + void utils.businesses.getAll.invalidate(); + if (id) void utils.businesses.getById.invalidate({ id }); + Alert.alert("Default updated", "This business is now your default."); + }, + onError: (err) => Alert.alert("Could not set default", err.message), + }); + + if (!id) { + return ; + } + + if (businessQuery.isLoading) { + return ; + } + + const business = businessQuery.data; + if (!business) { + return ; + } + + return ( + + + + + + {business.name} + {business.isDefault ? Default : null} + + {business.nickname ? {business.nickname} : null} + {business.email ? {business.email} : null} + {business.phone ? {business.phone} : null} + {business.website ? {business.website} : null} + + + + {business.taxId ? ( + + ) : null} + + + + {(business.addressLine1 || business.city || business.state) && ( + + {business.addressLine1 ? ( + {business.addressLine1} + ) : null} + {business.addressLine2 ? ( + {business.addressLine2} + ) : null} + {(business.city || business.state || business.postalCode) && ( + + {[business.city, business.state, business.postalCode].filter(Boolean).join(", ")} + + )} + {business.country ? {business.country} : null} + + )} + + +