Increase max password generator length to 256 chars in mobile app (#1701)

This commit is contained in:
Leendert de Borst
2026-02-14 21:16:02 +01:00
committed by Leendert de Borst
parent 438528a123
commit 18b61029ea
3 changed files with 90 additions and 33 deletions

View File

@@ -7,6 +7,7 @@ import { StyleSheet, View, TouchableOpacity, Switch, Platform } from 'react-nati
import type { PasswordSettings } from '@/utils/dist/core/models/vault';
import { CreatePasswordGenerator } from '@/utils/dist/core/password-generator';
import { sliderToLength, lengthToSlider, SLIDER_MIN, SLIDER_MAX } from '@/utils/passwordLengthSlider';
import { useColors } from '@/hooks/useColorScheme';
import { useVaultMutate } from '@/hooks/useVaultMutate';
@@ -60,7 +61,7 @@ export default function PasswordGeneratorSettingsScreen(): React.ReactNode {
const passwordSettings = await dbContext.sqliteClient!.getPasswordSettings();
setSettings(passwordSettings);
setSliderValue(passwordSettings.Length);
setSliderValue(lengthToSlider(passwordSettings.Length));
initialValues.current = passwordSettings;
// Generate initial preview password only once
@@ -119,19 +120,19 @@ export default function PasswordGeneratorSettingsScreen(): React.ReactNode {
* Handle slider value change.
*/
const handleSliderChange = useCallback((value: number): void => {
const roundedLength = Math.round(value);
setSliderValue(roundedLength);
setSliderValue(value);
const passwordLength = sliderToLength(value);
// Only generate if value actually changed and we're actively sliding
if (roundedLength !== lastGeneratedLength.current && isSliding.current && settings) {
lastGeneratedLength.current = roundedLength;
if (passwordLength !== lastGeneratedLength.current && isSliding.current && settings) {
lastGeneratedLength.current = passwordLength;
// Update settings and regenerate password
const newSettings = { ...settings, Length: roundedLength };
const newSettings = { ...settings, Length: passwordLength };
setSettings(newSettings);
// Track the change
pendingChanges.current = { ...pendingChanges.current, Length: roundedLength };
pendingChanges.current = { ...pendingChanges.current, Length: passwordLength };
// Generate new preview password
try {
@@ -149,7 +150,7 @@ export default function PasswordGeneratorSettingsScreen(): React.ReactNode {
*/
const handleSliderStart = useCallback((): void => {
isSliding.current = true;
lastGeneratedLength.current = sliderValue ?? 0;
lastGeneratedLength.current = sliderToLength(sliderValue ?? 0);
}, [sliderValue]);
/**
@@ -157,18 +158,18 @@ export default function PasswordGeneratorSettingsScreen(): React.ReactNode {
*/
const handleSliderComplete = useCallback((value: number): void => {
isSliding.current = false;
const roundedLength = Math.round(value);
const passwordLength = sliderToLength(value);
if (!settings) {
return;
}
// Update settings with final value
const newSettings = { ...settings, Length: roundedLength };
const newSettings = { ...settings, Length: passwordLength };
setSettings(newSettings);
// Track the change
pendingChanges.current = { ...pendingChanges.current, Length: roundedLength };
pendingChanges.current = { ...pendingChanges.current, Length: passwordLength };
// Generate password with final value
try {
@@ -325,7 +326,7 @@ export default function PasswordGeneratorSettingsScreen(): React.ReactNode {
<View style={styles.previewContainer}>
<ThemedText style={styles.previewLabel}>{t('settings.passwordGeneratorSettings.preview')}</ThemedText>
<View style={styles.previewInputContainer}>
<ThemedText style={styles.previewInput}>{previewPassword}</ThemedText>
<ThemedText style={styles.previewInput} numberOfLines={1} ellipsizeMode="tail">{previewPassword}</ThemedText>
<TouchableOpacity
style={styles.refreshButton}
onPress={handleRefreshPreview}
@@ -340,17 +341,16 @@ export default function PasswordGeneratorSettingsScreen(): React.ReactNode {
<View style={styles.sliderContainer}>
<View style={styles.sliderHeader}>
<ThemedText style={styles.sliderLabel}>{t('items.passwordLength')}</ThemedText>
<ThemedText style={styles.sliderValue}>{sliderValue ?? 0}</ThemedText>
<ThemedText style={styles.sliderValue}>{sliderToLength(sliderValue ?? 0)}</ThemedText>
</View>
<Slider
style={styles.slider}
minimumValue={8}
maximumValue={64}
minimumValue={SLIDER_MIN}
maximumValue={SLIDER_MAX}
value={sliderValue ?? 0}
onValueChange={handleSliderChange}
onSlidingStart={handleSliderStart}
onSlidingComplete={handleSliderComplete}
step={1}
minimumTrackTintColor={colors.primary}
maximumTrackTintColor={colors.accentBorder}
thumbTintColor={colors.primary}

View File

@@ -6,6 +6,7 @@ import { View, TextInput, TextInputProps, StyleSheet, TouchableOpacity, Platform
import type { PasswordSettings } from '@/utils/dist/core/models/vault';
import { CreatePasswordGenerator } from '@/utils/dist/core/password-generator';
import { sliderToLength, lengthToSlider, SLIDER_MIN, SLIDER_MAX } from '@/utils/passwordLengthSlider';
import { useColors } from '@/hooks/useColorScheme';
@@ -58,12 +59,12 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
// Initialize slider value immediately from initialSettings or value length, otherwise default to 16
const [sliderValue, setSliderValue] = useState<number>(() => {
if (initialSettings) {
return initialSettings.Length;
return lengthToSlider(initialSettings.Length);
}
if (!isNewCredential && value && value.length > 0) {
return value.length;
return lengthToSlider(value.length);
}
return 16;
return lengthToSlider(16);
});
const lastGeneratedLength = useRef<number>(0);
const isSliding = useRef(false);
@@ -92,7 +93,7 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
setCurrentSettings(settings);
// Only update slider if we haven't set it from value yet
if (!hasSetInitialLength.current) {
setSliderValue(settings.Length);
setSliderValue(lengthToSlider(settings.Length));
hasSetInitialLength.current = true;
}
}
@@ -107,7 +108,7 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
useEffect(() => {
if (!hasSetInitialLength.current) {
if (!isNewCredential && value && value.length > 0) {
setSliderValue(value.length);
setSliderValue(lengthToSlider(value.length));
hasSetInitialLength.current = true;
} else if (isNewCredential) {
hasSetInitialLength.current = true;
@@ -146,17 +147,17 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
}, [currentSettings, generatePassword, onChangeText, setShowPasswordState]);
const handleSliderChange = useCallback((sliderVal: number) => {
const roundedLength = Math.round(sliderVal);
setSliderValue(roundedLength);
setSliderValue(sliderVal);
const passwordLength = sliderToLength(sliderVal);
if (roundedLength !== lastGeneratedLength.current && isSliding.current) {
lastGeneratedLength.current = roundedLength;
if (passwordLength !== lastGeneratedLength.current && isSliding.current) {
lastGeneratedLength.current = passwordLength;
if (!showPassword) {
setShowPasswordState(true);
}
const newSettings = { ...(currentSettings || {}), Length: roundedLength } as PasswordSettings;
const newSettings = { ...(currentSettings || {}), Length: passwordLength } as PasswordSettings;
if (currentSettings) {
const password = generatePassword(newSettings);
if (password) {
@@ -168,14 +169,14 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
const handleSliderStart = useCallback(() => {
isSliding.current = true;
lastGeneratedLength.current = sliderValue;
lastGeneratedLength.current = sliderToLength(sliderValue);
}, [sliderValue]);
const handleSliderComplete = useCallback((sliderVal: number) => {
isSliding.current = false;
const roundedLength = Math.round(sliderVal);
const passwordLength = sliderToLength(sliderVal);
if (currentSettings) {
const newSettings = { ...currentSettings, Length: roundedLength };
const newSettings = { ...currentSettings, Length: passwordLength };
setCurrentSettings(newSettings);
}
lastGeneratedLength.current = 0;
@@ -531,7 +532,7 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
<View style={styles.sliderHeader}>
<ThemedText style={styles.sliderLabel}>{t('items.passwordLength')}</ThemedText>
<View style={styles.sliderValueContainer}>
<ThemedText style={styles.sliderValue}>{sliderValue}</ThemedText>
<ThemedText style={styles.sliderValue}>{sliderToLength(sliderValue)}</ThemedText>
<TouchableOpacity
style={styles.settingsButton}
onPress={handleOpenSettings}
@@ -544,13 +545,12 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
<Slider
style={styles.slider}
minimumValue={8}
maximumValue={64}
minimumValue={SLIDER_MIN}
maximumValue={SLIDER_MAX}
value={sliderValue}
onValueChange={handleSliderChange}
onSlidingStart={handleSliderStart}
onSlidingComplete={handleSliderComplete}
step={1}
minimumTrackTintColor={colors.primary}
maximumTrackTintColor={colors.accentBorder}
thumbTintColor={colors.primary}

View File

@@ -0,0 +1,57 @@
/**
* Utility functions for password length slider with non-linear scaling.
*
* The slider uses a power curve to provide fine-grained control at lower values
* (where most users operate, e.g., 12-32 chars) and coarser control at higher values
* (64-256 chars).
*
* This makes it easy to select common password lengths while still allowing
* very long passwords when needed.
*/
/** Minimum password length */
export const MIN_PASSWORD_LENGTH = 8;
/** Maximum password length */
export const MAX_PASSWORD_LENGTH = 256;
/** Slider minimum value (internal representation) */
export const SLIDER_MIN = 0;
/** Slider maximum value (internal representation) */
export const SLIDER_MAX = 100;
/**
* Exponent for the power curve.
* Higher values = more precision at lower lengths.
* 2.0 gives a good balance where ~50% slider = ~70 chars
*/
const EXPONENT = 2.0;
/**
* Convert a slider position (0-100) to an actual password length (8-256).
* Uses a power curve for non-linear scaling.
*
* @param sliderValue - The slider position (0-100)
* @returns The password length (8-256)
*/
export function sliderToLength(sliderValue: number): number {
const normalized = Math.max(0, Math.min(1, sliderValue / SLIDER_MAX));
const curved = Math.pow(normalized, EXPONENT);
const length = MIN_PASSWORD_LENGTH + curved * (MAX_PASSWORD_LENGTH - MIN_PASSWORD_LENGTH);
return Math.round(length);
}
/**
* Convert a password length (8-256) to a slider position (0-100).
* Inverse of sliderToLength.
*
* @param length - The password length (8-256)
* @returns The slider position (0-100)
*/
export function lengthToSlider(length: number): number {
const clampedLength = Math.max(MIN_PASSWORD_LENGTH, Math.min(MAX_PASSWORD_LENGTH, length));
const normalized = (clampedLength - MIN_PASSWORD_LENGTH) / (MAX_PASSWORD_LENGTH - MIN_PASSWORD_LENGTH);
const curved = Math.pow(normalized, 1 / EXPONENT);
return curved * SLIDER_MAX;
}