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
+28
View File
@@ -0,0 +1,28 @@
import type { ReactNode } from "react";
import { StyleSheet, View, type StyleProp, type ViewStyle } from "react-native";
import { GlassSurface } from "@/components/GlassSurface";
import { spacing } from "@/constants/theme";
const AUTH_CARD_RADIUS = 24;
type AuthCardProps = {
children: ReactNode;
style?: StyleProp<ViewStyle>;
};
export function AuthCard({ children, style }: AuthCardProps) {
return (
<GlassSurface radius={AUTH_CARD_RADIUS} style={style}>
<View style={styles.inner}>{children}</View>
</GlassSurface>
);
}
const styles = StyleSheet.create({
inner: {
paddingHorizontal: spacing.lg,
paddingVertical: spacing.lg,
gap: spacing.lg,
},
});
+45
View File
@@ -0,0 +1,45 @@
import { StyleSheet, Text, View } from "react-native";
import { Logo } from "@/components/Logo";
import { fonts, spacing } from "@/constants/theme";
import { useAppTheme } from "@/contexts/ThemeContext";
type AuthCardHeaderProps = {
title: string;
description: string;
};
export function AuthCardHeader({ title, description }: AuthCardHeaderProps) {
const { colors } = useAppTheme();
return (
<View style={styles.wrapper}>
<Logo size="md" />
<View style={styles.copy}>
<Text style={[styles.title, { color: colors.foreground }]}>{title}</Text>
<Text style={[styles.description, { color: colors.mutedForeground }]}>
{description}
</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
wrapper: {
gap: spacing.md,
},
copy: {
gap: spacing.xs,
},
title: {
fontSize: 24,
fontFamily: fonts.headingSemi,
letterSpacing: -0.3,
},
description: {
fontSize: 14,
fontFamily: fonts.body,
lineHeight: 20,
},
});
+34
View File
@@ -0,0 +1,34 @@
import { StyleSheet, Text, View } from "react-native";
import { fonts, spacing } from "@/constants/theme";
import { useAppTheme } from "@/contexts/ThemeContext";
export function AuthDivider() {
const { colors } = useAppTheme();
return (
<View style={styles.row}>
<View style={[styles.line, { backgroundColor: colors.border }]} />
<Text style={[styles.label, { color: colors.mutedForeground }]}>or</Text>
<View style={[styles.line, { backgroundColor: colors.border }]} />
</View>
);
}
const styles = StyleSheet.create({
row: {
flexDirection: "row",
alignItems: "center",
gap: spacing.sm,
},
line: {
flex: 1,
height: StyleSheet.hairlineWidth,
},
label: {
fontSize: 12,
fontFamily: fonts.bodyMedium,
textTransform: "uppercase",
letterSpacing: 0.6,
},
});
+39
View File
@@ -0,0 +1,39 @@
import { StyleSheet, Text } from "react-native";
import { fonts, radii, spacing } from "@/constants/theme";
import { useAppTheme } from "@/contexts/ThemeContext";
type AuthNoticeProps = {
children: string;
};
export function AuthNotice({ children }: AuthNoticeProps) {
const { colors } = useAppTheme();
return (
<Text
style={[
styles.notice,
{
color: colors.mutedForeground,
backgroundColor: colors.muted,
borderColor: colors.border,
},
]}
>
{children}
</Text>
);
}
const styles = StyleSheet.create({
notice: {
fontSize: 14,
fontFamily: fonts.body,
lineHeight: 20,
borderWidth: StyleSheet.hairlineWidth,
borderRadius: radii.lg,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm + 2,
},
});
+47
View File
@@ -0,0 +1,47 @@
import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, View, type ViewProps } from "react-native";
import { AuthBackground } from "@/components/AppBackground";
import { FullScreen } from "@/components/Screen";
import { spacing } from "@/constants/theme";
export function AuthScreenLayout({ children, style, ...props }: ViewProps) {
return (
<AuthBackground>
<FullScreen style={styles.safe}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : undefined}
style={styles.flex}
>
<ScrollView
contentContainerStyle={styles.container}
keyboardShouldPersistTaps="handled"
>
<View style={[styles.content, style]} {...props}>
{children}
</View>
</ScrollView>
</KeyboardAvoidingView>
</FullScreen>
</AuthBackground>
);
}
const styles = StyleSheet.create({
safe: {
flex: 1,
},
flex: {
flex: 1,
},
container: {
flexGrow: 1,
justifyContent: "center",
paddingHorizontal: spacing.lg,
paddingVertical: spacing.xl,
},
content: {
width: "100%",
maxWidth: 420,
alignSelf: "center",
},
});