Add beenvoice mobile companion app with full dark mode support.
Expo app with dashboard, time clock, invoices, and settings — native tabs, glass UI, theme-aware components, and iOS Live Activities. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
import { Link, router } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { FullScreen } from "@/components/Screen";
|
||||
import { AuthBackground } from "@/components/AppBackground";
|
||||
import { CollapsibleServerField } from "@/components/CollapsibleServerField";
|
||||
import { HeadingText, Logo } from "@/components/Logo";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { Input } from "@/components/ui/Input";
|
||||
import { fonts, spacing } from "@/constants/theme";
|
||||
import { useAccounts } from "@/contexts/AccountsContext";
|
||||
import { useAuthClient } from "@/contexts/AuthContext";
|
||||
import { useAppTheme } from "@/contexts/ThemeContext";
|
||||
import { registerAccount } from "@/lib/auth-api";
|
||||
|
||||
export default function RegisterScreen() {
|
||||
const authClient = useAuthClient();
|
||||
const { apiUrl, registerAccount: saveAccount } = useAccounts();
|
||||
const { colors } = useAppTheme();
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleRegister() {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await registerAccount({
|
||||
firstName: firstName.trim(),
|
||||
lastName: lastName.trim(),
|
||||
email: email.trim(),
|
||||
password,
|
||||
});
|
||||
|
||||
const { error: signInError } = await authClient.signIn.email({
|
||||
email: email.trim(),
|
||||
password,
|
||||
});
|
||||
|
||||
if (signInError) {
|
||||
router.replace("/(auth)/sign-in");
|
||||
return;
|
||||
}
|
||||
|
||||
const session = await authClient.getSession();
|
||||
const user = session.data?.user;
|
||||
if (user) {
|
||||
await saveAccount({
|
||||
instanceUrl: apiUrl,
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
});
|
||||
}
|
||||
|
||||
router.replace("/(app)");
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Registration failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthBackground>
|
||||
<FullScreen style={styles.safe}>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
||||
style={styles.flex}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.container}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<Card style={styles.card}>
|
||||
<View style={styles.header}>
|
||||
<Logo size="lg" />
|
||||
<HeadingText style={styles.title}>Create your account</HeadingText>
|
||||
<Text style={[styles.subtitle, { color: colors.mutedForeground }]}>
|
||||
Get started today
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.form}>
|
||||
<View style={styles.row}>
|
||||
<View style={styles.half}>
|
||||
<Input
|
||||
label="First name"
|
||||
value={firstName}
|
||||
onChangeText={setFirstName}
|
||||
autoComplete="given-name"
|
||||
placeholder="Jane"
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.half}>
|
||||
<Input
|
||||
label="Last name"
|
||||
value={lastName}
|
||||
onChangeText={setLastName}
|
||||
autoComplete="family-name"
|
||||
placeholder="Doe"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Input
|
||||
label="Email"
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
keyboardType="email-address"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
<Input
|
||||
label="Password"
|
||||
secureTextEntry
|
||||
autoComplete="new-password"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
placeholder="At least 8 characters"
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
<Text style={[styles.error, { color: colors.destructive }]}>{error}</Text>
|
||||
) : null}
|
||||
|
||||
<Button title="Create Account" loading={loading} onPress={handleRegister} />
|
||||
</View>
|
||||
|
||||
<Text style={[styles.footer, { color: colors.mutedForeground }]}>
|
||||
Already have an account?{" "}
|
||||
<Link href="/(auth)/sign-in" style={[styles.link, { color: colors.foreground }]}>
|
||||
Sign in
|
||||
</Link>
|
||||
</Text>
|
||||
</Card>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
<CollapsibleServerField />
|
||||
</FullScreen>
|
||||
</AuthBackground>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1 },
|
||||
flex: { flex: 1 },
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
padding: spacing.lg,
|
||||
paddingBottom: spacing.md,
|
||||
},
|
||||
card: {
|
||||
gap: spacing.lg,
|
||||
},
|
||||
header: { gap: spacing.sm },
|
||||
title: { fontSize: 24, marginTop: spacing.sm },
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
fontFamily: fonts.body,
|
||||
},
|
||||
form: { gap: spacing.md },
|
||||
row: { flexDirection: "row", gap: spacing.md },
|
||||
half: { flex: 1 },
|
||||
error: {
|
||||
fontSize: 14,
|
||||
fontFamily: fonts.body,
|
||||
},
|
||||
footer: {
|
||||
textAlign: "center",
|
||||
fontSize: 14,
|
||||
fontFamily: fonts.body,
|
||||
},
|
||||
link: {
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user