import { Button } from "@/components/Button"; import { Input } from "@/components/common/Input"; import { Text } from "@/components/common/Text"; import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider"; import { Ionicons } from "@expo/vector-icons"; import { PublicSystemInfo } from "@jellyfin/sdk/lib/generated-client"; import { getSystemApi } from "@jellyfin/sdk/lib/utils/api"; import { Image } from "expo-image"; import { useLocalSearchParams } from "expo-router"; import { useAtom } from "jotai"; import React, { useEffect, useState } from "react"; import { Alert, KeyboardAvoidingView, Platform, SafeAreaView, View, } from "react-native"; import { z } from "zod"; const CredentialsSchema = z.object({ username: z.string().min(1, "Username is 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 [error, setError] = 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 [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) { setError(error.message); } else { setError("An unexpected error occurred"); } } 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. */ async function checkUrl(url: string) { url = url.endsWith("/") ? url.slice(0, -1) : url; setLoadingServerCheck(true); const protocols = ["https://", "http://"]; const timeout = 2000; // 2 seconds timeout for long 404 responses try { for (const protocol of protocols) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(`${protocol}${url}/System/Info/Public`, { mode: "cors", signal: controller.signal, }); clearTimeout(timeoutId); if (response.ok) { const data = (await response.json()) as PublicSystemInfo; setServerName(data.ServerName || ""); return `${protocol}${url}`; } } catch (e) { const error = e as Error; if (error.name === "AbortError") { console.error(`Request to ${protocol}${url} timed out`); } else { console.error(`Error checking ${protocol}${url}:`, error); } } } 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 = async (url: string) => { url = url.trim(); const result = await checkUrl( url.startsWith("http") ? new URL(url).host : url ); if (result === undefined) { Alert.alert( "Connection failed", "Could not connect to the server. Please check the URL and your network connection." ); return; } setServer({ address: result }); }; const handleQuickConnect = async () => { try { const code = await initiateQuickConnect(); if (code) { Alert.alert("Quick Connect", `Enter code ${code} to login`, [ { text: "Got It", }, ]); } } catch (error) { Alert.alert("Error", "Failed to initiate Quick Connect"); } }; if (api?.basePath) { return ( {serverName || "Streamyfin"} URL {api.basePath} Log in 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} /> {error} ); } return ( Streamyfin Connect to your Jellyfin server ); }; export default Login;