diff --git a/app/(auth)/(tabs)/(home)/settings.tv.tsx b/app/(auth)/(tabs)/(home)/settings.tv.tsx index 7a32fc9c..9dd56673 100644 --- a/app/(auth)/(tabs)/(home)/settings.tv.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tv.tsx @@ -2,7 +2,7 @@ import { SubtitlePlaybackMode } from "@jellyfin/sdk/lib/generated-client"; import { useAtom } from "jotai"; import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { ScrollView, View } from "react-native"; +import { Alert, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Text } from "@/components/common/Text"; import { TVPasswordEntryModal } from "@/components/login/TVPasswordEntryModal"; @@ -80,12 +80,26 @@ export default function SettingsTV() { const hasOtherAccounts = otherAccounts.length > 0; // Handle account selection from modal - const handleAccountSelect = (account: SavedServerAccount) => { + const handleAccountSelect = async (account: SavedServerAccount) => { if (!currentServer) return; if (account.securityType === "none") { // Direct login with saved credential - loginWithSavedCredential(currentServer.address, account.userId); + try { + await loginWithSavedCredential(currentServer.address, account.userId); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : t("server.session_expired"); + const isSessionExpired = errorMessage.includes( + t("server.session_expired"), + ); + Alert.alert( + isSessionExpired + ? t("server.session_expired") + : t("login.connection_failed"), + isSessionExpired ? t("server.please_login_again") : errorMessage, + ); + } } else if (account.securityType === "pin") { // Show PIN modal setSelectedServer(currentServer); @@ -103,10 +117,24 @@ export default function SettingsTV() { const handlePinSuccess = async () => { setPinModalVisible(false); if (selectedServer && selectedAccount) { - await loginWithSavedCredential( - selectedServer.address, - selectedAccount.userId, - ); + try { + await loginWithSavedCredential( + selectedServer.address, + selectedAccount.userId, + ); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : t("server.session_expired"); + const isSessionExpired = errorMessage.includes( + t("server.session_expired"), + ); + Alert.alert( + isSessionExpired + ? t("server.session_expired") + : t("login.connection_failed"), + isSessionExpired ? t("server.please_login_again") : errorMessage, + ); + } } setSelectedServer(null); setSelectedAccount(null); diff --git a/components/PreviousServersList.tsx b/components/PreviousServersList.tsx index 008e1be2..251a6ca3 100644 --- a/components/PreviousServersList.tsx +++ b/components/PreviousServersList.tsx @@ -73,10 +73,19 @@ export const PreviousServersList: React.FC = ({ setLoadingServer(server.address); try { await onQuickLogin(server.address, account.userId); - } catch { - Alert.alert( + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : t("server.session_expired"); + const isSessionExpired = errorMessage.includes( t("server.session_expired"), - t("server.please_login_again"), + ); + Alert.alert( + isSessionExpired + ? t("server.session_expired") + : t("login.connection_failed"), + isSessionExpired ? t("server.please_login_again") : errorMessage, [{ text: t("common.ok"), onPress: () => onServerSelect(server) }], ); } finally { @@ -122,10 +131,17 @@ export const PreviousServersList: React.FC = ({ setLoadingServer(selectedServer.address); try { await onQuickLogin(selectedServer.address, selectedAccount.userId); - } catch { - Alert.alert( + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : t("server.session_expired"); + const isSessionExpired = errorMessage.includes( t("server.session_expired"), - t("server.please_login_again"), + ); + Alert.alert( + isSessionExpired + ? t("server.session_expired") + : t("login.connection_failed"), + isSessionExpired ? t("server.please_login_again") : errorMessage, [ { text: t("common.ok"), diff --git a/components/login/Login.tsx b/components/login/Login.tsx index 0b1fedc7..7cf5f43f 100644 --- a/components/login/Login.tsx +++ b/components/login/Login.tsx @@ -72,22 +72,24 @@ export const Login: React.FC = () => { password: string; } | null>(null); + // Handle URL params for server connection useEffect(() => { (async () => { if (_apiUrl) { await setServer({ address: _apiUrl, }); - - setTimeout(() => { - if (_username && _password) { - setCredentials({ username: _username, password: _password }); - login(_username, _password); - } - }, 0); } })(); - }, [_apiUrl, _username, _password]); + }, [_apiUrl]); + + // Handle auto-login when api is ready and credentials are provided via URL params + useEffect(() => { + if (api?.basePath && _apiUrl && _username && _password) { + setCredentials({ username: _username, password: _password }); + login(_username, _password); + } + }, [api?.basePath, _apiUrl, _username, _password]); useEffect(() => { navigation.setOptions({ diff --git a/components/login/TVLogin.tsx b/components/login/TVLogin.tsx index 0453ed9c..2ddfac78 100644 --- a/components/login/TVLogin.tsx +++ b/components/login/TVLogin.tsx @@ -122,19 +122,21 @@ export const TVLogin: React.FC = () => { }; }, [stopQuickConnectPolling]); - // Auto login from URL params + // Handle URL params for server connection useEffect(() => { (async () => { if (_apiUrl) { await setServer({ address: _apiUrl }); - setTimeout(() => { - if (_username && _password) { - login(_username, _password); - } - }, 0); } })(); - }, [_apiUrl, _username, _password]); + }, [_apiUrl]); + + // Handle auto-login when api is ready and credentials are provided via URL params + useEffect(() => { + if (api?.basePath && _apiUrl && _username && _password) { + login(_username, _password); + } + }, [api?.basePath, _apiUrl, _username, _password]); // Update header useEffect(() => { @@ -263,10 +265,19 @@ export const TVLogin: React.FC = () => { setLoading(true); try { await loginWithSavedCredential(currentServer.address, account.userId); - } catch { - Alert.alert( + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : t("server.session_expired"); + const isSessionExpired = errorMessage.includes( t("server.session_expired"), - t("server.please_login_again"), + ); + Alert.alert( + isSessionExpired + ? t("server.session_expired") + : t("login.connection_failed"), + isSessionExpired ? t("server.please_login_again") : errorMessage, [ { text: t("common.ok"), @@ -301,10 +312,17 @@ export const TVLogin: React.FC = () => { currentServer.address, selectedAccount.userId, ); - } catch { - Alert.alert( + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : t("server.session_expired"); + const isSessionExpired = errorMessage.includes( t("server.session_expired"), - t("server.please_login_again"), + ); + Alert.alert( + isSessionExpired + ? t("server.session_expired") + : t("login.connection_failed"), + isSessionExpired ? t("server.please_login_again") : errorMessage, ); } finally { setLoading(false); diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index 1b066ba3..ebdaaca1 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -427,14 +427,33 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ // Refresh plugin settings await refreshStreamyfinPluginSettings(); } catch (error) { - // Token is invalid/expired - remove it - if ( - axios.isAxiosError(error) && - (error.response?.status === 401 || error.response?.status === 403) - ) { - await deleteAccountCredential(serverUrl, userId); - throw new Error(t("server.session_expired")); + // Check for axios error + if (axios.isAxiosError(error)) { + // Token is invalid/expired - remove it + if ( + error.response?.status === 401 || + error.response?.status === 403 + ) { + await deleteAccountCredential(serverUrl, userId); + throw new Error(t("server.session_expired")); + } + + // Network error - server not reachable (no response means server didn't respond) + if (!error.response) { + throw new Error(t("home.server_unreachable")); + } } + + // Check for network error by message pattern (fallback detection) + if ( + error instanceof Error && + (error.message.toLowerCase().includes("network") || + error.message.toLowerCase().includes("econnrefused") || + error.message.toLowerCase().includes("timeout")) + ) { + throw new Error(t("home.server_unreachable")); + } + throw error; } },