Move production to beenvoice.app with migrated accounts, refreshed auth and timer UX, and expanded invoice flows.

Official URL migration preserves sessions, shortcuts prefs, and last clock-in client; auth screens match web with legal links; time clock and invoice editor/send flows are updated for the new domain and UI patterns.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 03:40:48 -04:00
co-authored by Cursor
parent e17c4c6854
commit 6762a9bff3
60 changed files with 2544 additions and 1091 deletions
@@ -0,0 +1,155 @@
import { Ionicons } from "@expo/vector-icons";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { fonts, radii, spacing } from "@/constants/theme";
import { useAppTheme } from "@/contexts/ThemeContext";
import type { InvoiceStatus } from "@/lib/invoice-status";
type ActionItem = {
key: string;
title: string;
subtitle?: string;
icon: keyof typeof Ionicons.glyphMap;
onPress: () => void;
loading?: boolean;
};
type InvoiceDetailActionsProps = {
status: InvoiceStatus;
clientEmail: string;
onPaymentReminder?: () => void;
paymentReminderLoading?: boolean;
onUpdateStatus: () => void;
updateStatusLoading?: boolean;
onTrackTime: () => void;
};
export function InvoiceDetailActions({
status,
clientEmail,
onPaymentReminder,
paymentReminderLoading,
onUpdateStatus,
updateStatusLoading,
onTrackTime,
}: InvoiceDetailActionsProps) {
const { colors } = useAppTheme();
const rows: ActionItem[] = [];
if ((status === "sent" || status === "overdue") && onPaymentReminder) {
rows.push({
key: "reminder",
title: "Send payment reminder",
subtitle: clientEmail ? `Nudge ${clientEmail}` : "Add a client email first",
icon: "notifications-outline",
onPress: onPaymentReminder,
loading: paymentReminderLoading,
});
}
rows.push(
{
key: "status",
title: "Update status",
subtitle: "Draft, sent, or paid",
icon: "swap-horizontal-outline",
onPress: onUpdateStatus,
loading: updateStatusLoading,
},
{
key: "timer",
title: "Track time",
subtitle: "Clock hours to this invoice",
icon: "timer-outline",
onPress: onTrackTime,
},
);
return (
<View
style={[
styles.card,
{
borderColor: colors.border,
backgroundColor: colors.cardGlass,
},
]}
>
<View style={styles.list}>
{rows.map((row, index) => (
<View key={row.key}>
{index > 0 ? (
<View style={[styles.divider, { backgroundColor: colors.border }]} />
) : null}
<Pressable
accessibilityRole="button"
disabled={row.loading}
onPress={row.onPress}
style={({ pressed }) => [styles.row, pressed && styles.rowPressed]}
>
<View style={[styles.iconWrap, { backgroundColor: colors.muted }]}>
<Ionicons name={row.icon} size={20} color={colors.foreground} />
</View>
<View style={styles.copy}>
<Text style={[styles.title, { color: colors.foreground }]}>{row.title}</Text>
{row.subtitle ? (
<Text style={[styles.subtitle, { color: colors.mutedForeground }]}>
{row.subtitle}
</Text>
) : null}
</View>
<Ionicons name="chevron-forward" size={18} color={colors.mutedForeground} />
</Pressable>
</View>
))}
</View>
</View>
);
}
const styles = StyleSheet.create({
card: {
borderWidth: 1,
borderRadius: radii.lg,
padding: spacing.md,
gap: spacing.sm,
},
list: {
gap: 0,
},
divider: {
height: StyleSheet.hairlineWidth,
marginVertical: spacing.xs,
},
row: {
flexDirection: "row",
alignItems: "center",
gap: spacing.sm,
paddingVertical: spacing.sm,
},
rowPressed: {
opacity: 0.75,
},
iconWrap: {
width: 40,
height: 40,
borderRadius: radii.md,
alignItems: "center",
justifyContent: "center",
},
copy: {
flex: 1,
gap: 2,
minWidth: 0,
},
title: {
fontFamily: fonts.bodySemiBold,
fontSize: 15,
},
subtitle: {
fontFamily: fonts.body,
fontSize: 13,
lineHeight: 18,
},
});