fix(auth): distinguish session expiry from network errors

This commit is contained in:
Fredrik Burmester
2026-02-01 12:27:22 +01:00
parent fea3e1449a
commit d17414bc93
5 changed files with 124 additions and 41 deletions

View File

@@ -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);

View File

@@ -73,10 +73,19 @@ export const PreviousServersList: React.FC<PreviousServersListProps> = ({
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<PreviousServersListProps> = ({
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"),

View File

@@ -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({

View File

@@ -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);

View File

@@ -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;
}
},