mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
Some checks failed
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (phone) (push) Has been cancelled
🤖 Android APK Build (Phone + TV) / 🏗️ Build Android APK (tv) (push) Has been cancelled
🤖 iOS IPA Build (Phone + TV) / 🏗️ Build iOS IPA (phone) (push) Has been cancelled
🔒 Lockfile Consistency Check / 🔍 Check bun.lock and package.json consistency (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (actions) (push) Has been cancelled
🛡️ CodeQL Analysis / 🔎 Analyze with CodeQL (javascript-typescript) (push) Has been cancelled
🏷️🔀Merge Conflict Labeler / 🏷️ Labeling Merge Conflicts (push) Has been cancelled
🚦 Security & Quality Gate / 📝 Validate PR Title (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Vulnerable Dependencies (push) Has been cancelled
🚦 Security & Quality Gate / 🚑 Expo Doctor Check (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (check) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (format) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (lint) (push) Has been cancelled
🚦 Security & Quality Gate / 🔍 Lint & Test (typecheck) (push) Has been cancelled
🕒 Handle Stale Issues / 🗑️ Cleanup Stale Issues (push) Has been cancelled
112 lines
3.2 KiB
TypeScript
112 lines
3.2 KiB
TypeScript
import { Ionicons } from "@expo/vector-icons";
|
|
import type React from "react";
|
|
import { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { TouchableOpacity } from "react-native";
|
|
import { Input } from "./common/Input";
|
|
|
|
// Discriminated union for password visibility control
|
|
type PasswordVisibilityControlled = {
|
|
value?: string;
|
|
onChangeText: (text: string) => void;
|
|
placeholder: string;
|
|
showPassword: boolean;
|
|
onShowPasswordChange: (show: boolean) => void;
|
|
topOffset?: number;
|
|
layout?: "tv" | "mobile";
|
|
};
|
|
|
|
type PasswordVisibilityUncontrolled = {
|
|
value?: string;
|
|
onChangeText: (text: string) => void;
|
|
placeholder: string;
|
|
showPassword?: never;
|
|
onShowPasswordChange?: never;
|
|
topOffset?: number;
|
|
layout?: "tv" | "mobile";
|
|
defaultShowPassword?: boolean;
|
|
};
|
|
|
|
type PasswordInputProps =
|
|
| PasswordVisibilityControlled
|
|
| PasswordVisibilityUncontrolled;
|
|
|
|
export const PasswordInput: React.FC<PasswordInputProps> = (props) => {
|
|
const { t } = useTranslation();
|
|
const {
|
|
value = "",
|
|
onChangeText,
|
|
placeholder,
|
|
topOffset = 14, // Default 14px for mobile
|
|
layout = "mobile",
|
|
} = props;
|
|
|
|
// Type guard to check if we're in controlled mode
|
|
const isControlled =
|
|
"showPassword" in props && "onShowPasswordChange" in props;
|
|
|
|
// Internal state for uncontrolled mode
|
|
const [internalShowPassword, setInternalShowPassword] = useState(() =>
|
|
!isControlled && "defaultShowPassword" in props
|
|
? ((props as PasswordVisibilityUncontrolled).defaultShowPassword ?? false)
|
|
: false,
|
|
);
|
|
|
|
// Use controlled value if available, otherwise use internal state
|
|
const showPassword = isControlled
|
|
? (props as PasswordVisibilityControlled).showPassword
|
|
: internalShowPassword;
|
|
|
|
const handleTogglePassword = () => {
|
|
if (isControlled) {
|
|
(props as PasswordVisibilityControlled).onShowPasswordChange(
|
|
!showPassword,
|
|
);
|
|
} else {
|
|
// For uncontrolled mode, toggle internal state
|
|
setInternalShowPassword(!showPassword);
|
|
}
|
|
};
|
|
|
|
// Generate top position style with validation
|
|
const getTopStyle = () => {
|
|
if (typeof topOffset !== "number" || Number.isNaN(topOffset)) {
|
|
console.warn(`Invalid topOffset value: ${topOffset}`);
|
|
return { top: 14 }; // Default fallback (14px for mobile)
|
|
}
|
|
return { top: topOffset };
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Input
|
|
placeholder={placeholder}
|
|
onChangeText={onChangeText}
|
|
value={value}
|
|
secureTextEntry={!showPassword}
|
|
extraClassName='pr-4'
|
|
/>
|
|
<TouchableOpacity
|
|
onPress={handleTogglePassword}
|
|
className={`absolute right-3 p-1 ${
|
|
layout === "tv" ? "h-10 justify-center" : ""
|
|
}`}
|
|
style={getTopStyle()}
|
|
accessible={true}
|
|
accessibilityRole='button'
|
|
accessibilityLabel={
|
|
showPassword ? t("login.hide_password") : t("login.show_password")
|
|
}
|
|
accessibilityHint={t("login.toggle_password_visibility")}
|
|
accessibilityState={{ checked: showPassword }}
|
|
>
|
|
<Ionicons
|
|
name={showPassword ? "eye-off" : "eye"}
|
|
size={24}
|
|
color='white'
|
|
/>
|
|
</TouchableOpacity>
|
|
</>
|
|
);
|
|
};
|