import { Trans, useLingui } from "@lingui/react/macro"; import { IconServer2 } from "@tabler/icons-react-native"; import { useForm, useStore } from "@tanstack/react-form"; import { useQuery } from "@tanstack/react-query"; import { Link } from "expo-router"; import { useMemo, useRef, useState } from "react"; import { Pressable, type TextInput, View } from "react-native"; import Animated, { FadeIn, FadeInDown } from "react-native-reanimated"; import { useCSSVariable } from "uniwind"; import { z } from "zod"; import { AuthScreen } from "@/components/auth-screen"; import { Button, ButtonLabel } from "@/components/ui/button"; import { ScaledIcon } from "@/components/ui/scaled-icon"; import { Spinner } from "@/components/ui/spinner"; import { Text } from "@/components/ui/text"; import { Input, Label, TextField } from "@/components/ui/text-field"; import { getAuthErrorMessage } from "@/lib/error-messages"; import { orpc } from "@/lib/orpc"; import { queryClient } from "@/lib/query-client"; import { authClient, getServerUrl, splitUrl } from "@/lib/server"; import { toast } from "@/lib/toast"; import { getFormErrors } from "@/utils/form-errors"; import * as Haptics from "@/utils/haptics"; export default function LoginScreen() { const { t } = useLingui(); const signInSchema = useMemo( () => z.object({ email: z .string() .trim() .min(1, t`Email is required`) .email(t`Enter a valid email address`), password: z.string().min(1, t`Password is required`), }), [t], ); const passwordRef = useRef(null); const [errorFields, setErrorFields] = useState>(new Set()); const [isSignedIn, setIsSignedIn] = useState(false); const publicInfo = useQuery(orpc.system.publicInfo.queryOptions()); const form = useForm({ defaultValues: { email: "", password: "" }, onSubmit: async ({ value }) => { const result = signInSchema.safeParse(value); if (!result.success) { const { message, fields } = getFormErrors(result.error); setErrorFields(fields); toast.error(message); return; } await authClient.signIn.email( { email: result.data.email, password: result.data.password }, { onError(error) { toast.error(getAuthErrorMessage(error.error, t`Failed to sign in`)); }, onSuccess() { Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); setIsSignedIn(true); queryClient.invalidateQueries(); }, }, ); }, }); const isSubmitting = useStore(form.store, (s) => s.isSubmitting); const busy = isSubmitting || isSignedIn; const statusCompletedColor = useCSSVariable("--color-status-completed") as string; const serverHost = splitUrl(getServerUrl()).host; const showPasswordLogin = !publicInfo.data?.passwordLoginDisabled; const showOidc = publicInfo.data?.oidcEnabled; const showRegister = publicInfo.data?.registrationOpen; const clearFieldError = (name: string) => { if (errorFields.has(name)) { setErrorFields((prev) => { const next = new Set(prev); next.delete(name); return next; }); } }; return ( {showOidc && (() => { const providerName = publicInfo.data?.oidcProviderName ?? "SSO"; return ( {showPasswordLogin && ( or )} ); })()} {showPasswordLogin && ( {(field) => ( { field.handleChange(text); clearFieldError("email"); }} placeholder="wwhite@graymatter.biz" keyboardType="email-address" autoCapitalize="none" autoComplete="email" textContentType="emailAddress" returnKeyType="next" blurOnSubmit={false} onSubmitEditing={() => passwordRef.current?.focus()} className={errorFields.has("email") ? "border-destructive" : undefined} /> )} {(field) => ( { field.handleChange(text); clearFieldError("password"); }} placeholder="••••••••" secureTextEntry autoComplete="password" textContentType="password" returnKeyType="go" onSubmitEditing={form.handleSubmit} className={errorFields.has("password") ? "border-destructive" : undefined} /> )} {showRegister && ( )} )} Connected to {serverHost}. Tap to change. ); }