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"; import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; import { PublicSystemInfo } from "@jellyfin/sdk/lib/generated-client"; import { Image } from "expo-image"; import { useLocalSearchParams, useNavigation } from "expo-router"; import { useAtom } from "jotai"; import React, { useCallback, useEffect, useState } from "react"; import { Alert, KeyboardAvoidingView, Platform, SafeAreaView, TouchableOpacity, View, } from "react-native"; import { z } from "zod"; import { t } from 'i18next'; const CredentialsSchema = z.object({ username: z.string().min(1, t("login.username_required")),}); const Login: React.FC = () => { const { setServer, login, removeServer, initiateQuickConnect } = useJellyfin(); const [api] = useAtom(apiAtom); const params = useLocalSearchParams(); const { apiUrl: _apiUrl, username: _username, password: _password, } = params as { apiUrl: string; username: string; password: string }; const [serverURL, setServerURL] = useState(_apiUrl); const [serverName, setServerName] = useState(""); const [credentials, setCredentials] = useState<{ username: string; password: string; }>({ username: _username, password: _password, }); useEffect(() => { (async () => { // we might re-use the checkUrl function here to check the url as well // however, I don't think it should be necessary for now if (_apiUrl) { setServer({ address: _apiUrl, }); setTimeout(() => { if (_username && _password) { setCredentials({ username: _username, password: _password }); login(_username, _password); } }, 300); } })(); }, [_apiUrl, _username, _password]); const navigation = useNavigation(); 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 [loading, setLoading] = useState(false); const handleLogin = async () => { 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); } }; const [loadingServerCheck, setLoadingServerCheck] = useState(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; } 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} autoFocus secureTextEntry={false} keyboardType="default" returnKeyType="done" autoCapitalize="none" textContentType="username" 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); } handleConnect(server.address); }} /> { handleConnect(s.address); }} /> )} ); }; export default Login;