6d2711e36e
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>
178 lines
4.9 KiB
TypeScript
178 lines
4.9 KiB
TypeScript
import { Link, router } from "expo-router";
|
|
import { useState } from "react";
|
|
import {
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
Pressable,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
} from "react-native";
|
|
import { AuthBackground } from "@/components/AppBackground";
|
|
import { HeadingText, Logo } from "@/components/Logo";
|
|
import { FullScreen } from "@/components/Screen";
|
|
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";
|
|
|
|
export default function SignInScreen() {
|
|
const authClient = useAuthClient();
|
|
const { apiUrl, registerAccount } = useAccounts();
|
|
const { colors } = useAppTheme();
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
async function handleSignIn() {
|
|
setError(null);
|
|
setLoading(true);
|
|
|
|
const { error: signInError } = await authClient.signIn.email({ email: email.trim(), password });
|
|
|
|
if (signInError) {
|
|
setLoading(false);
|
|
const message = signInError.message ?? "";
|
|
if (message.toLowerCase().includes("internal") || message.includes("500")) {
|
|
setError("Server error — is the API running with Postgres? Check beenvoice dev + docker.");
|
|
} else {
|
|
setError(message || "Invalid email or password");
|
|
}
|
|
return;
|
|
}
|
|
|
|
const session = await authClient.getSession();
|
|
const user = session.data?.user;
|
|
if (user) {
|
|
await registerAccount({
|
|
instanceUrl: apiUrl,
|
|
userId: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
});
|
|
}
|
|
|
|
setLoading(false);
|
|
router.replace("/(app)");
|
|
}
|
|
|
|
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}>Welcome back</HeadingText>
|
|
<Text style={[styles.subtitle, { color: colors.mutedForeground }]}>
|
|
Sign in to manage invoices on the go
|
|
</Text>
|
|
</View>
|
|
|
|
<View style={styles.form}>
|
|
<Input
|
|
label="Email"
|
|
autoCapitalize="none"
|
|
autoComplete="email"
|
|
keyboardType="email-address"
|
|
value={email}
|
|
onChangeText={setEmail}
|
|
placeholder="you@example.com"
|
|
/>
|
|
<Input
|
|
label="Password"
|
|
secureTextEntry
|
|
autoComplete="password"
|
|
value={password}
|
|
onChangeText={setPassword}
|
|
placeholder="••••••••"
|
|
/>
|
|
|
|
<Pressable onPress={() => router.push("/(auth)/forgot-password")}>
|
|
<Text style={[styles.forgot, { color: colors.mutedForeground }]}>
|
|
Forgot password?
|
|
</Text>
|
|
</Pressable>
|
|
|
|
{error ? (
|
|
<Text style={[styles.error, { color: colors.destructive }]}>{error}</Text>
|
|
) : null}
|
|
|
|
<Button title="Sign In" loading={loading} onPress={handleSignIn} />
|
|
</View>
|
|
|
|
<Text style={[styles.footer, { color: colors.mutedForeground }]}>
|
|
Don't have an account?{" "}
|
|
<Link href="/(auth)/register" style={[styles.link, { color: colors.foreground }]}>
|
|
Create one
|
|
</Link>
|
|
</Text>
|
|
</Card>
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
</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,
|
|
},
|
|
forgot: {
|
|
alignSelf: "flex-end",
|
|
fontFamily: fonts.bodyMedium,
|
|
fontSize: 12,
|
|
},
|
|
error: {
|
|
fontSize: 14,
|
|
fontFamily: fonts.body,
|
|
},
|
|
footer: {
|
|
textAlign: "center",
|
|
fontSize: 14,
|
|
fontFamily: fonts.body,
|
|
},
|
|
link: {
|
|
fontFamily: fonts.bodySemiBold,
|
|
},
|
|
});
|