mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-18 17:18:11 +00:00
refactor: replace inline password inputs with reusable PasswordInput component
Consolidates duplicate password input implementations across login and settings screens into a single reusable component. Improves code maintainability by eliminating redundant password visibility toggle logic and standardizing password input behavior throughout the application. Adds consistent accessibility support and test identifiers across all password input instances.
This commit is contained in:
@@ -20,6 +20,7 @@ import { Button } from "@/components/Button";
|
||||
import { Input } from "@/components/common/Input";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import JellyfinServerDiscovery from "@/components/JellyfinServerDiscovery";
|
||||
import { PasswordInput } from "@/components/PasswordInput";
|
||||
import { PreviousServersList } from "@/components/PreviousServersList";
|
||||
import { Colors } from "@/constants/Colors";
|
||||
import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider";
|
||||
@@ -276,33 +277,20 @@ const Login: React.FC = () => {
|
||||
/>
|
||||
|
||||
{/* Password */}
|
||||
<View className='relative mb-4'>
|
||||
<Input
|
||||
placeholder={t("login.password_placeholder")}
|
||||
onChangeText={(text: string) =>
|
||||
setCredentials({ ...credentials, password: text })
|
||||
}
|
||||
value={credentials.password}
|
||||
secureTextEntry={!showPassword}
|
||||
keyboardType='default'
|
||||
returnKeyType='done'
|
||||
autoCapitalize='none'
|
||||
textContentType='password'
|
||||
clearButtonMode='while-editing'
|
||||
maxLength={500}
|
||||
className='pr-12'
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowPassword(!showPassword)}
|
||||
className='absolute right-3 top-4 p-1'
|
||||
>
|
||||
<Ionicons
|
||||
name={showPassword ? "eye-off" : "eye"}
|
||||
size={24}
|
||||
color='white'
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<PasswordInput
|
||||
value={credentials.password}
|
||||
onChangeText={(text: string) =>
|
||||
setCredentials({ ...credentials, password: text })
|
||||
}
|
||||
onSubmitEditing={handleLogin}
|
||||
placeholder={t("login.password_placeholder")}
|
||||
className='mb-4'
|
||||
showPassword={showPassword}
|
||||
onShowPasswordChange={setShowPassword}
|
||||
topPosition='4'
|
||||
testID='tv-password-input'
|
||||
accessibilityLabel={t("login.password_placeholder")}
|
||||
/>
|
||||
|
||||
<View className='mt-4'>
|
||||
<Button onPress={handleLogin}>{t("login.login_button")}</Button>
|
||||
@@ -417,33 +405,18 @@ const Login: React.FC = () => {
|
||||
maxLength={500}
|
||||
/>
|
||||
|
||||
<View className='relative'>
|
||||
<Input
|
||||
placeholder={t("login.password_placeholder")}
|
||||
onChangeText={(text) =>
|
||||
setCredentials({ ...credentials, password: text })
|
||||
}
|
||||
value={credentials.password}
|
||||
secureTextEntry={!showPassword}
|
||||
keyboardType='default'
|
||||
returnKeyType='done'
|
||||
autoCapitalize='none'
|
||||
textContentType='password'
|
||||
clearButtonMode='while-editing'
|
||||
maxLength={500}
|
||||
className='pr-12'
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowPassword(!showPassword)}
|
||||
className='absolute right-3 top-3.5 p-1'
|
||||
>
|
||||
<Ionicons
|
||||
name={showPassword ? "eye-off" : "eye"}
|
||||
size={24}
|
||||
color='white'
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<PasswordInput
|
||||
value={credentials.password}
|
||||
onChangeText={(text) =>
|
||||
setCredentials({ ...credentials, password: text })
|
||||
}
|
||||
placeholder={t("login.password_placeholder")}
|
||||
showPassword={showPassword}
|
||||
onShowPasswordChange={setShowPassword}
|
||||
topPosition='3.5'
|
||||
testID='mobile-password-input'
|
||||
accessibilityLabel={t("login.password_placeholder")}
|
||||
/>
|
||||
<View className='flex flex-row items-center justify-between'>
|
||||
<Button
|
||||
onPress={handleLogin}
|
||||
|
||||
101
components/PasswordInput.tsx
Normal file
101
components/PasswordInput.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { Input } from "./common/Input";
|
||||
|
||||
interface PasswordInputProps {
|
||||
value?: string;
|
||||
onChangeText: (text: string) => void;
|
||||
onSubmitEditing?: () => void;
|
||||
placeholder: string;
|
||||
className?: string;
|
||||
testID?: string;
|
||||
accessibilityLabel?: string;
|
||||
maxLength?: number;
|
||||
showPassword?: boolean;
|
||||
onShowPasswordChange?: (show: boolean) => void;
|
||||
topPosition?: "3.5" | "4";
|
||||
editable?: boolean;
|
||||
autoComplete?:
|
||||
| "password"
|
||||
| "username"
|
||||
| "name"
|
||||
| "email"
|
||||
| "tel"
|
||||
| "url"
|
||||
| "off";
|
||||
autoCorrect?: boolean;
|
||||
}
|
||||
|
||||
export const PasswordInput: React.FC<PasswordInputProps> = ({
|
||||
value = "",
|
||||
onChangeText,
|
||||
onSubmitEditing,
|
||||
placeholder,
|
||||
className = "",
|
||||
testID,
|
||||
accessibilityLabel,
|
||||
maxLength = 500,
|
||||
showPassword: controlledShowPassword,
|
||||
onShowPasswordChange,
|
||||
topPosition = "3.5",
|
||||
editable = true,
|
||||
autoComplete,
|
||||
autoCorrect,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [internalShowPassword, setInternalShowPassword] = useState(false);
|
||||
|
||||
// Use controlled state if provided, otherwise use internal state
|
||||
const showPassword = controlledShowPassword ?? internalShowPassword;
|
||||
const setShowPassword = onShowPasswordChange ?? setInternalShowPassword;
|
||||
|
||||
const topClass = topPosition === "4" ? "top-4" : "top-3.5";
|
||||
|
||||
return (
|
||||
<View className={`relative ${className}`}>
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
onChangeText={onChangeText}
|
||||
value={value}
|
||||
secureTextEntry={!showPassword}
|
||||
keyboardType='default'
|
||||
returnKeyType='done'
|
||||
autoCapitalize='none'
|
||||
textContentType='password'
|
||||
clearButtonMode='while-editing'
|
||||
maxLength={maxLength}
|
||||
className='pr-12'
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
testID={testID}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
editable={editable}
|
||||
autoComplete={autoComplete}
|
||||
autoCorrect={autoCorrect}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowPassword(!showPassword)}
|
||||
className={`absolute right-3 ${topClass} p-1`}
|
||||
accessible={true}
|
||||
accessibilityRole='button'
|
||||
accessibilityLabel={
|
||||
showPassword
|
||||
? t("accessibility.hide_password")
|
||||
: t("accessibility.show_password")
|
||||
}
|
||||
accessibilityHint={t("accessibility.toggle_password_visibility")}
|
||||
accessibilityState={{ selected: showPassword }}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
testID={testID ? `${testID}-toggle` : undefined}
|
||||
>
|
||||
<Ionicons
|
||||
name={showPassword ? "eye-off" : "eye"}
|
||||
size={24}
|
||||
color='white'
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useAtom } from "jotai";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import { toast } from "sonner-native";
|
||||
import { PasswordInput } from "@/components/PasswordInput";
|
||||
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
|
||||
import { userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
@@ -150,37 +150,24 @@ export const JellyseerrSettings = () => {
|
||||
<Text className='font-bold mb-2'>
|
||||
{t("home.settings.plugins.jellyseerr.password")}
|
||||
</Text>
|
||||
<View className='relative'>
|
||||
<Input
|
||||
className='border border-neutral-800 pr-12'
|
||||
autoFocus={true}
|
||||
focusable={true}
|
||||
placeholder={t(
|
||||
"home.settings.plugins.jellyseerr.password_placeholder",
|
||||
{ username: user?.Name },
|
||||
)}
|
||||
value={jellyseerrPassword}
|
||||
keyboardType='default'
|
||||
secureTextEntry={!showJellyseerrPassword}
|
||||
returnKeyType='done'
|
||||
autoCapitalize='none'
|
||||
textContentType='password'
|
||||
onChangeText={setJellyseerrPassword}
|
||||
editable={!loginToJellyseerrMutation.isPending}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={() =>
|
||||
setShowJellyseerrPassword(!showJellyseerrPassword)
|
||||
}
|
||||
className='absolute right-3 top-3.5 p-1'
|
||||
>
|
||||
<Ionicons
|
||||
name={showJellyseerrPassword ? "eye-off" : "eye"}
|
||||
size={24}
|
||||
color='white'
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<PasswordInput
|
||||
value={jellyseerrPassword}
|
||||
onChangeText={setJellyseerrPassword}
|
||||
placeholder={t(
|
||||
"home.settings.plugins.jellyseerr.password_placeholder",
|
||||
{ username: user?.Name },
|
||||
)}
|
||||
className='border border-neutral-800'
|
||||
showPassword={showJellyseerrPassword}
|
||||
onShowPasswordChange={setShowJellyseerrPassword}
|
||||
editable={!loginToJellyseerrMutation.isPending}
|
||||
autoComplete='password'
|
||||
autoCorrect={false}
|
||||
testID='jellyseerr-password-input'
|
||||
accessibilityLabel={t(
|
||||
"home.settings.plugins.jellyseerr.password",
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
loading={loginToJellyseerrMutation.isPending}
|
||||
disabled={loginToJellyseerrMutation.isPending}
|
||||
|
||||
Reference in New Issue
Block a user