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 <cursoragent@cursor.com>
This commit is contained in:
2026-06-17 23:14:58 -04:00
co-authored by Cursor
parent 14c880123c
commit 6d2711e36e
41 changed files with 2410 additions and 181 deletions
+4 -3
View File
@@ -1,6 +1,7 @@
import Constants from "expo-constants";
const fallbackUrl = "http://localhost:3000";
/** Production API used by default (App Store review + production builds). */
export const DEFAULT_API_URL = "https://beenvoice.soconnor.dev";
let runtimeOverride: string | null = null;
@@ -15,10 +16,10 @@ export function getApiUrl() {
if (fromEnv) return fromEnv.replace(/\/$/, "");
const hostUri = Constants.expoConfig?.hostUri;
if (hostUri) {
if (hostUri && __DEV__) {
const host = hostUri.split(":")[0];
if (host) return `http://${host}:3000`;
}
return fallbackUrl;
return DEFAULT_API_URL;
}
+11
View File
@@ -0,0 +1,11 @@
/** Matches web invoice-form default numbering. */
export function generateInvoiceNumber(): string {
const date = new Date().toISOString().slice(0, 10).replace(/-/g, "");
return `INV-${date}-${String(Date.now()).slice(-6)}`;
}
export function defaultDueDate(issueDate: Date): Date {
const due = new Date(issueDate);
due.setDate(due.getDate() + 30);
return due;
}
+14 -8
View File
@@ -6,8 +6,8 @@ import { spacing } from "@/constants/theme";
/** Standard UITabBar content height (home indicator is separate). */
const IOS_TAB_BAR_HEIGHT = 49;
/** Slightly less than measured inset so content sits closer to the tab bar. */
const TAB_BAR_PADDING_TRIM = spacing.sm;
/** Trim extra inset so scroll content sits closer to the tab bar. */
const TAB_BAR_PADDING_TRIM = spacing.lg;
/**
* Pixels between the bottom of the safe-area layout frame and the window bottom.
@@ -31,17 +31,23 @@ export function useNativeTabBarHeight(): number {
/**
* Bottom padding so scroll content can clear the floating native tab bar.
* Uses layout-frame measurement when available, otherwise tab bar + home indicator.
* Uses tab bar + home indicator as the target — layout-frame measurement can
* over-report and leave a large empty scroll gap above the tab bar.
*/
export function useTabBarScrollPadding(): number {
const belowLayoutFrame = useBelowLayoutFrame();
const { bottom: homeIndicator } = useSafeAreaInsets();
const tabBar = useNativeTabBarHeight();
const clearance = tabBar + homeIndicator;
return Math.max(spacing.xs, clearance - TAB_BAR_PADDING_TRIM);
}
/** Bottom offset for floating action buttons above the tab bar. */
export function useFloatingActionBottom(): number {
const { bottom: homeIndicator } = useSafeAreaInsets();
const tabBar = useNativeTabBarHeight();
const raw =
belowLayoutFrame > 0 ? belowLayoutFrame : tabBar + homeIndicator;
return Math.max(tabBar + homeIndicator - TAB_BAR_PADDING_TRIM, raw - TAB_BAR_PADDING_TRIM);
return tabBar + homeIndicator + spacing.xs;
}
/** @deprecated Use useTabBarScrollPadding */
+8
View File
@@ -51,6 +51,10 @@ export type ThemeColors = {
brandDark: string;
text: string;
textMuted: string;
switchTrackOn: string;
switchTrackOff: string;
switchThumb: string;
switchIosBackground: string;
};
export function getThemeColors(scheme: ColorSchemeName): ThemeColors {
@@ -83,6 +87,10 @@ export function getThemeColors(scheme: ColorSchemeName): ThemeColors {
brandDark: palette?.foreground ?? light.foreground,
text: palette?.foreground ?? light.foreground,
textMuted: palette?.mutedForeground ?? light.mutedForeground,
switchTrackOn: isDark ? (palette?.success ?? "#4ADE80") : light.primary,
switchTrackOff: isDark ? "#3F3F46" : light.border,
switchThumb: isDark ? "#FAFAFA" : "#FFFFFF",
switchIosBackground: isDark ? "#3F3F46" : light.border,
};
}