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
+147 -143
View File
@@ -25,28 +25,10 @@ type LineItemEditorProps = {
isLast?: boolean;
};
export function LineItemsTableHeader() {
function FieldLabel({ children }: { children: string }) {
const { colors } = useAppTheme();
return (
<View style={[headerStyles.row, { borderBottomColor: colors.border }]}>
<Text style={[headerStyles.cell, headerStyles.desc, { color: colors.mutedForeground }]}>
Description
</Text>
<Text style={[headerStyles.cell, headerStyles.date, { color: colors.mutedForeground }]}>
Date
</Text>
<Text style={[headerStyles.cell, headerStyles.hours, { color: colors.mutedForeground }]}>
Hrs
</Text>
<Text style={[headerStyles.cell, headerStyles.rate, { color: colors.mutedForeground }]}>
Rate
</Text>
<Text style={[headerStyles.cell, headerStyles.amt, { color: colors.mutedForeground }]}>
Amt
</Text>
<View style={headerStyles.spacer} />
</View>
<Text style={[styles.fieldLabel, { color: colors.mutedForeground }]}>{children}</Text>
);
}
@@ -68,20 +50,20 @@ export function LineItemEditor({
return (
<View
style={[
styles.row,
!isLast && { borderBottomColor: colors.border, borderBottomWidth: 1 },
styles.readBlock,
!isLast && { borderBottomColor: colors.border, borderBottomWidth: StyleSheet.hairlineWidth },
]}
>
<Text style={[styles.index, { color: colors.mutedForeground }]}>{index + 1}</Text>
<View style={styles.descCol}>
<Text style={[styles.readTitle, { color: colors.foreground }]} numberOfLines={2}>
{item.description.trim() || "Untitled line"}
</Text>
<Text style={[styles.readSub, { color: colors.mutedForeground }]}>
{formatShortDate(item.date)} · {hours}h × {formatCurrency(rate, currency)}
</Text>
</View>
<Text style={[styles.amount, { color: colors.foreground }]}>
<Text style={[styles.readIndex, { color: colors.mutedForeground }]}>
Line {index + 1}
</Text>
<Text style={[styles.readTitle, { color: colors.foreground }]} numberOfLines={3}>
{item.description.trim() || "Untitled line"}
</Text>
<Text style={[styles.readSub, { color: colors.mutedForeground }]}>
{formatShortDate(item.date)} · {hours}h × {formatCurrency(rate, currency)}
</Text>
<Text style={[styles.readAmount, { color: colors.foreground }]}>
{formatCurrency(amount, currency)}
</Text>
</View>
@@ -92,154 +74,159 @@ export function LineItemEditor({
<View
style={[
styles.editBlock,
!isLast && { borderBottomColor: colors.border, borderBottomWidth: 1 },
!isLast && { borderBottomColor: colors.border, borderBottomWidth: StyleSheet.hairlineWidth },
]}
>
<View style={styles.editTop}>
<Text style={[styles.index, { color: colors.mutedForeground }]}>{index + 1}</Text>
<TextInput
value={item.description}
onChangeText={(description) => onChange({ description })}
placeholder="What was done?"
placeholderTextColor={colors.mutedForeground}
style={[
styles.descriptionInput,
{
color: colors.foreground,
borderColor: colors.border,
backgroundColor: colors.cardGlass,
},
]}
/>
</View>
<Text style={[styles.lineLabel, { color: colors.mutedForeground }]}>Line {index + 1}</Text>
<View style={styles.metricsRow}>
<CompactDateField
value={item.date}
onChange={(date) => onChange({ date })}
style={styles.dateField}
/>
<CompactStepperInput
value={item.hours}
onChangeText={(hours) => onChange({ hours })}
step={0.25}
style={styles.hoursField}
/>
<View style={[styles.rateField, { borderColor: colors.border, backgroundColor: colors.cardGlass }]}>
<Text style={[styles.ratePrefix, { color: colors.mutedForeground }]}>$</Text>
<TextInput
value={item.rate}
onChangeText={(rate) => onChange({ rate })}
keyboardType="decimal-pad"
placeholder="0"
placeholderTextColor={colors.mutedForeground}
style={[styles.rateInput, { color: colors.foreground }]}
<TextInput
value={item.description}
onChangeText={(description) => onChange({ description })}
placeholder="What was done?"
placeholderTextColor={colors.mutedForeground}
style={[
styles.descriptionInput,
{
color: colors.foreground,
borderColor: colors.border,
backgroundColor: colors.cardGlass,
},
]}
/>
<View style={styles.fieldsRow}>
<View style={styles.fieldCol}>
<FieldLabel>Date</FieldLabel>
<CompactDateField
value={item.date}
onChange={(date) => onChange({ date })}
style={styles.fieldControl}
/>
</View>
<Text style={[styles.amount, styles.amountEdit, { color: colors.foreground }]}>
{formatCurrency(amount, currency)}
</Text>
<View style={styles.fieldCol}>
<FieldLabel>Hours</FieldLabel>
<CompactStepperInput
value={item.hours}
onChangeText={(hours) => onChange({ hours })}
step={0.25}
style={styles.fieldControl}
/>
</View>
<View style={styles.fieldCol}>
<FieldLabel>Rate</FieldLabel>
<View
style={[
styles.rateField,
{ borderColor: colors.border, backgroundColor: colors.cardGlass },
]}
>
<Text style={[styles.ratePrefix, { color: colors.mutedForeground }]}>$</Text>
<TextInput
value={item.rate}
onChangeText={(rate) => onChange({ rate })}
keyboardType="decimal-pad"
placeholder="0"
placeholderTextColor={colors.mutedForeground}
style={[styles.rateInput, { color: colors.foreground }]}
/>
</View>
</View>
</View>
<View style={styles.footerRow}>
<View style={styles.amountGroup}>
<Text style={[styles.amountLabel, { color: colors.mutedForeground }]}>Amount</Text>
<Text style={[styles.amountValue, { color: colors.foreground }]}>
{formatCurrency(amount, currency)}
</Text>
</View>
<Pressable
accessibilityRole="button"
accessibilityLabel="Remove line item"
onPress={onRemove}
hitSlop={8}
style={({ pressed }) => [styles.remove, pressed && styles.removePressed]}
style={({ pressed }) => [
styles.remove,
{ borderColor: colors.border, backgroundColor: colors.cardGlass },
pressed && styles.removePressed,
]}
>
<Ionicons name="trash-outline" size={17} color={colors.destructive} />
<Ionicons name="trash-outline" size={18} color={colors.destructive} />
</Pressable>
</View>
</View>
);
}
const headerStyles = StyleSheet.create({
row: {
flexDirection: "row",
alignItems: "center",
const styles = StyleSheet.create({
readBlock: {
paddingVertical: spacing.md,
gap: spacing.xs,
paddingBottom: spacing.xs,
marginBottom: spacing.xs,
borderBottomWidth: 1,
},
cell: {
readIndex: {
fontFamily: fonts.bodySemiBold,
fontSize: 11,
textTransform: "uppercase",
letterSpacing: 0.4,
},
desc: { flex: 1, paddingLeft: 22 },
date: { width: 72 },
hours: { width: 88, textAlign: "center" },
rate: { width: 72, textAlign: "center" },
amt: { width: 64, textAlign: "right" },
spacer: { width: 32 },
});
const styles = StyleSheet.create({
row: {
flexDirection: "row",
alignItems: "center",
gap: spacing.xs,
paddingVertical: spacing.sm,
},
editBlock: {
paddingVertical: spacing.sm,
gap: spacing.xs,
},
editTop: {
flexDirection: "row",
alignItems: "center",
gap: spacing.xs,
},
index: {
width: 18,
fontFamily: fonts.bodySemiBold,
fontSize: 12,
textAlign: "center",
},
descCol: {
flex: 1,
gap: 2,
},
readTitle: {
fontFamily: fonts.bodyMedium,
fontSize: 14,
lineHeight: 18,
fontSize: 15,
lineHeight: 20,
},
readSub: {
fontFamily: fonts.body,
fontSize: 13,
},
readAmount: {
fontFamily: fonts.bodySemiBold,
fontSize: 15,
marginTop: 2,
},
editBlock: {
paddingVertical: spacing.md,
gap: spacing.sm,
},
lineLabel: {
fontFamily: fonts.bodySemiBold,
fontSize: 11,
textTransform: "uppercase",
letterSpacing: 0.4,
},
descriptionInput: {
flex: 1,
minHeight: 36,
width: "100%",
minHeight: 40,
borderWidth: 1,
borderRadius: radii.md,
paddingHorizontal: spacing.sm,
fontFamily: fonts.body,
fontSize: 14,
paddingVertical: 6,
fontSize: 15,
paddingVertical: 8,
},
metricsRow: {
fieldsRow: {
flexDirection: "row",
alignItems: "center",
gap: spacing.xs,
paddingLeft: 22,
gap: spacing.sm,
},
dateField: {
width: 72,
fieldCol: {
flex: 1,
gap: 4,
minWidth: 0,
},
hoursField: {
width: 88,
fieldLabel: {
fontFamily: fonts.bodyMedium,
fontSize: 11,
textTransform: "uppercase",
letterSpacing: 0.3,
},
fieldControl: {
width: "100%",
},
rateField: {
width: 72,
minHeight: 36,
flexDirection: "row",
alignItems: "center",
borderWidth: 1,
borderRadius: radii.md,
minHeight: 36,
paddingHorizontal: spacing.xs,
},
ratePrefix: {
@@ -248,23 +235,40 @@ const styles = StyleSheet.create({
},
rateInput: {
flex: 1,
fontFamily: fonts.body,
fontFamily: fonts.bodyMedium,
fontSize: 13,
paddingVertical: 4,
textAlign: "right",
minWidth: 0,
},
amount: {
width: 64,
fontFamily: fonts.bodySemiBold,
fontSize: 13,
textAlign: "right",
footerRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: spacing.sm,
marginTop: 2,
},
amountEdit: {
amountGroup: {
flex: 1,
flexDirection: "row",
alignItems: "baseline",
gap: spacing.sm,
},
amountLabel: {
fontFamily: fonts.bodyMedium,
fontSize: 12,
textTransform: "uppercase",
letterSpacing: 0.3,
},
amountValue: {
fontFamily: fonts.bodySemiBold,
fontSize: 17,
},
remove: {
width: 32,
height: 36,
width: 40,
height: 40,
borderRadius: radii.md,
borderWidth: 1,
alignItems: "center",
justifyContent: "center",
},