import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; import type { PublicSystemInfo } from "@jellyfin/sdk/lib/generated-client"; import { Image } from "expo-image"; import { useLocalSearchParams, useNavigation } from "expo-router"; import { t } from "i18next"; import { useAtomValue } from "jotai"; import type React from "react"; import { useCallback, useEffect, useState } from "react"; import { Alert, Keyboard, KeyboardAvoidingView, Platform, SafeAreaView, TouchableOpacity, View, } from "react-native"; import { z } from "zod"; import { Button } from "@/components/Button"; import { Input } from "@/components/common/Input"; import { Text } from "@/components/common/Text"; import JellyfinServerDiscovery from "@/components/JellyfinServerDiscovery"; import { PreviousServersList } from "@/components/PreviousServersList"; import { Colors } from "@/constants/Colors"; import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider"; const CredentialsSchema = z.object({ username: z.string().min(1, t("login.username_required")), }); const Login: React.FC = () => { const api = useAtomValue(apiAtom); const navigation = useNavigation(); const params = useLocalSearchParams(); const { setServer, login, removeServer, initiateQuickConnect } = useJellyfin(); const { apiUrl: _apiUrl, username: _username, password: _password, } = params as { apiUrl: string; username: string; password: string }; const [loadingServerCheck, setLoadingServerCheck] = useState(false); const [loading, setLoading] = useState(false); const [serverURL, setServerURL] = useState(_apiUrl); const [serverName, setServerName] = useState(""); const [credentials, setCredentials] = useState<{ username: string; password: string; }>({ username: _username, password: _password, }); /** * A way to auto login based on a link */ useEffect(() => { (async () => { if (_apiUrl) { await setServer({ address: _apiUrl, }); setTimeout(() => { if (_username && _password) { setCredentials({ username: _username, password: _password }); login(_username, _password); } }, 300); } })(); }, [_apiUrl, _username, _password]); useEffect(() => { navigation.setOptions({ headerTitle: serverName, headerLeft: () => api?.basePath ? ( { removeServer(); }} className='flex flex-row items-center' > {t("login.change_server")} ) : null, }); }, [serverName, navigation, api?.basePath]); const handleLogin = async () => { Keyboard.dismiss(); setLoading(true); try { const result = CredentialsSchema.safeParse(credentials); if (result.success) { await login(credentials.username, credentials.password); } } catch (error) { if (error instanceof Error) { Alert.alert(t("login.connection_failed"), error.message); } else { Alert.alert( t("login.connection_failed"), t("login.an_unexpected_error_occured"), ); } } finally { setLoading(false); } }; /** * Checks the availability and validity of a Jellyfin server URL. * * This function attempts to connect to a Jellyfin server using the provided URL. * It tries both HTTPS and HTTP protocols, with a timeout to handle long 404 responses. * * @param {string} url - The base URL of the Jellyfin server to check. * @returns {Promise} A Promise that resolves to: * - The full URL (including protocol) if a valid Jellyfin server is found. * - undefined if no valid server is found at the given URL. * * Side effects: * - Sets loadingServerCheck state to true at the beginning and false at the end. * - Logs errors and timeout information to the console. */ const checkUrl = useCallback(async (url: string) => { setLoadingServerCheck(true); try { const response = await fetch(`${url}/System/Info/Public`, { mode: "cors", }); if (response.ok) { const data = (await response.json()) as PublicSystemInfo; setServerName(data.ServerName || ""); return url; } return undefined; } catch { return undefined; } finally { setLoadingServerCheck(false); } }, []); /** * Handles the connection attempt to a Jellyfin server. * * This function trims the input URL, checks its validity using the `checkUrl` function, * and sets the server address if a valid connection is established. * * @param {string} url - The URL of the Jellyfin server to connect to. * * @returns {Promise} * * Side effects: * - Calls `checkUrl` to validate the server URL. * - Shows an alert if the connection fails. * - Sets the server address using `setServer` if the connection is successful. * */ const handleConnect = useCallback(async (url: string) => { url = url.trim().replace(/\/$/, ""); const result = await checkUrl(url); if (result === undefined) { Alert.alert( t("login.connection_failed"), t("login.could_not_connect_to_server"), ); return; } await setServer({ address: url }); }, []); const handleQuickConnect = async () => { try { const code = await initiateQuickConnect(); if (code) { Alert.alert( t("login.quick_connect"), t("login.enter_code_to_login", { code: code }), [ { text: t("login.got_it"), }, ], ); } } catch (_error) { Alert.alert( t("login.error_title"), t("login.failed_to_initiate_quick_connect"), ); } }; return ( {api?.basePath ? ( {serverName ? ( <> {`${t("login.login_to_title")} `} {serverName} ) : ( t("login.login_title") )} {api.basePath} setCredentials({ ...credentials, username: text }) } value={credentials.username} keyboardType='default' returnKeyType='done' autoCapitalize='none' // Changed from username to oneTimeCode because it is a known issue in RN // https://github.com/facebook/react-native/issues/47106#issuecomment-2521270037 textContentType='oneTimeCode' clearButtonMode='while-editing' maxLength={500} /> setCredentials({ ...credentials, password: text }) } value={credentials.password} secureTextEntry keyboardType='default' returnKeyType='done' autoCapitalize='none' textContentType='password' clearButtonMode='while-editing' maxLength={500} /> ) : ( Streamyfin {t("server.enter_url_to_jellyfin_server")} { setServerURL(server.address); if (server.serverName) { setServerName(server.serverName); } await handleConnect(server.address); }} /> { await handleConnect(s.address); }} /> )} ); }; export default Login;