From f0dfe59d7ba9a18edcb97a3dbde1da521ae27721 Mon Sep 17 00:00:00 2001 From: jainil Date: Tue, 18 Aug 2026 17:16:29 +0530 Subject: [PATCH 1/2] feat: add password generator utility function --- src/utils/generating.ts | 68 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/utils/generating.ts diff --git a/src/utils/generating.ts b/src/utils/generating.ts new file mode 100644 index 00000000..a928d336 --- /dev/null +++ b/src/utils/generating.ts @@ -0,0 +1,68 @@ +interface PasswordOptions { + length: number; + letters?: boolean; // A-Z + a-z (one option) + numbers?: boolean; // 0-9 + symbols?: boolean; // !@#$%^&* etc. +} + +export function generatePassword(options: PasswordOptions): string { + // Default: everything enabled + const { length, letters = true, numbers = true, symbols = true } = options; + + const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const lower = "abcdefghijklmnopqrstuvwxyz"; + const nums = "0123456789"; + const syms = "!@#$%^&*()_+-=[]{}|;:,.<>?"; + + let allChars = ""; + const guaranteed: string[] = []; + + if (letters) { + allChars += upper + lower; + // Guarantee at least one upper and one lower + guaranteed.push(upper[secureRandom(upper.length)]); + guaranteed.push(lower[secureRandom(lower.length)]); + } + + // Numbers + if (numbers) { + allChars += nums; + guaranteed.push(nums[secureRandom(nums.length)]); + } + + // Symbols + if (symbols) { + allChars += syms; + guaranteed.push(syms[secureRandom(syms.length)]); + } + + // Validation + if (!allChars) { + throw new Error("Please select at least one character type."); + } + + if (guaranteed.length > length) { + throw new Error("Too many required character types for the fixed length."); + } + + // Fill remaining characters + const passwordChars = [...guaranteed]; + while (passwordChars.length < length) { + passwordChars.push(allChars[secureRandom(allChars.length)]); + } + + // Shuffle so guaranteed characters are not always at the start + for (let i = passwordChars.length - 1; i > 0; i--) { + const j = secureRandom(i + 1); + [passwordChars[i], passwordChars[j]] = [passwordChars[j], passwordChars[i]]; + } + + return passwordChars.join(""); +} + +// Cryptographically secure random number +function secureRandom(max: number): number { + const array = new Uint32Array(1); + crypto.getRandomValues(array); + return array[0] % max; +} From 1783c5bff01ce73c8876eb51fb9d408b998428a4 Mon Sep 17 00:00:00 2001 From: Jeel Dobariya Date: Sat, 29 Aug 2026 12:44:03 +0530 Subject: [PATCH 2/2] feat: add password generator. Signed-off-by: Jeel Dobariya --- package-lock.json | 19 ++ package.json | 2 + src/app/(tabs)/_layout.tsx | 14 + src/app/(tabs)/password-generator.tsx | 420 ++++++++++++++++++++++++++ src/utils/generating.ts | 154 +++++++--- 5 files changed, 563 insertions(+), 46 deletions(-) create mode 100644 src/app/(tabs)/password-generator.tsx diff --git a/package-lock.json b/package-lock.json index 50d08a16..5f4938b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,14 @@ "name": "passcodes", "version": "1.0.0", "dependencies": { + "@react-native-community/slider": "5.2.0", "@react-native-vector-icons/fontawesome6": "^13.1.2", "@shopify/flash-list": "2.0.2", "babel-plugin-inline-import": "^3.0.0", "drizzle-orm": "^0.45.2", "expo": "~57.0.9", "expo-build-properties": "~57.0.8", + "expo-clipboard": "~57.0.1", "expo-constants": "~57.0.3", "expo-dev-client": "~57.0.10", "expo-file-system": "~57.0.0", @@ -3430,6 +3432,12 @@ } } }, + "node_modules/@react-native-community/slider": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-5.2.0.tgz", + "integrity": "sha512-484sH8aWEaSjxaZ7HT3YZ8CKDcNes2synko1vdEz5DFEdvKAduxKJTj22L/qBMD7rtIkfbX69DMzWDAGbOAV6w==", + "license": "MIT" + }, "node_modules/@react-native-masked-view/masked-view": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@react-native-masked-view/masked-view/-/masked-view-0.3.2.tgz", @@ -7224,6 +7232,17 @@ "node": ">=10" } }, + "node_modules/expo-clipboard": { + "version": "57.0.1", + "resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-57.0.1.tgz", + "integrity": "sha512-HWICri4+1ao7S6QEfcorxVumXDiDnx1guGGewjZgGJWLGxFYs0RgH8ujBs+lkTzBkMmlwADaWSlaesR+nDJt5Q==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-constants": { "version": "57.0.8", "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.8.tgz", diff --git a/package.json b/package.json index 929cae3f..21ac9582 100644 --- a/package.json +++ b/package.json @@ -18,12 +18,14 @@ "deploy:x86_64": "eas build --profile deploy-x86_64 --platform android --local" }, "dependencies": { + "@react-native-community/slider": "5.2.0", "@react-native-vector-icons/fontawesome6": "^13.1.2", "@shopify/flash-list": "2.0.2", "babel-plugin-inline-import": "^3.0.0", "drizzle-orm": "^0.45.2", "expo": "~57.0.9", "expo-build-properties": "~57.0.8", + "expo-clipboard": "~57.0.1", "expo-constants": "~57.0.3", "expo-dev-client": "~57.0.10", "expo-file-system": "~57.0.0", diff --git a/src/app/(tabs)/_layout.tsx b/src/app/(tabs)/_layout.tsx index 6357da9f..bcd81e2f 100644 --- a/src/app/(tabs)/_layout.tsx +++ b/src/app/(tabs)/_layout.tsx @@ -15,6 +15,20 @@ export default function TabLayout() { options={{ title: "Passcodes", tabBarIcon: ({ size, color }) => ( + + ), + }} + /> + ( ( + DEFAULT_PASSWORD_OPTIONS, + ); + + const [password, setPassword] = useState(() => + generatePassword(DEFAULT_PASSWORD_OPTIONS), + ); + + const strength = getPasswordStrength(options); + + /* + * Regenerate the password whenever any generator + * option changes. + */ + useEffect(() => { + setPassword(generatePassword(options)); + }, [options]); + + function updateOption(key: CharacterType) { + setOptions((current) => { + const enabledCharacterTypes = CHARACTER_OPTIONS.filter( + (option) => current[option.key], + ); + + /* + * Keep at least one character type enabled. + */ + if (current[key] && enabledCharacterTypes.length === 1) { + return current; + } + + return { + ...current, + [key]: !current[key], + }; + }); + } + + function updateLength(length: number) { + setOptions((current) => ({ + ...current, + length: Math.round(length), + })); + } + + function generate() { + setPassword(generatePassword(options)); + } + + async function copyPassword() { + if (!password) { + return; + } + + await Clipboard.setStringAsync(password); + } + + return ( + + + + Password Generator + + + {/* Generated Password */} + + + + + Generated Password + + + + + + + + + + + + + {/* Password Length */} + + + + + + Length + + + Choose the length of your password + + + + + {options.length} + + + + + + + {MIN_PASSWORD_LENGTH} + + {MAX_PASSWORD_LENGTH} + + + + + {/* Character Types */} + + + + + Character Types + + + Choose which characters can be used + + + + + {CHARACTER_OPTIONS.map((option, index) => ( + + updateOption(option.key)} + /> + + {index < CHARACTER_OPTIONS.length - 1 && } + + ))} + + + + + {/* Password Strength */} + + + + + + Password Strength + + + Based on length and character variety + + + + + {strength.label} + + + + + + + + + ); +} + +type GeneratorOptionProps = { + label: string; + example: string; + enabled: boolean; + onPress: () => void; +}; + +function GeneratorOption({ + label, + example, + enabled, + onPress, +}: GeneratorOptionProps) { + return ( + + ); +} + +function StrengthIndicator({ score }: { score: number }) { + return ( + + + {Array.from({ length: 5 }).map((_, index) => ( + + ))} + + + + Weak + + Strong + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingVertical: 12, + }, + + content: { + paddingHorizontal: 12, + paddingBottom: 96, + gap: 24, + }, + + title: { + marginTop: 8, + marginBottom: 4, + marginHorizontal: 4, + textAlign: "center", + }, + + cardContent: { + padding: 16, + gap: 16, + }, + + sectionLabel: { + opacity: 0.7, + }, + + actionContainer: { + flexDirection: "row-reverse", + gap: 8, + }, + + sectionHeader: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + gap: 16, + }, + + headerText: { + flex: 1, + }, + + description: { + opacity: 0.6, + marginTop: 3, + }, + + lengthValue: { + minWidth: 40, + textAlign: "center", + }, + + rangeLabels: { + flexDirection: "row", + justifyContent: "space-between", + opacity: 0.6, + }, + + optionList: { + marginHorizontal: -8, + }, + + optionButton: { + marginHorizontal: 0, + }, + + optionButtonContent: { + justifyContent: "flex-start", + }, + + strengthContainer: { + gap: 8, + }, + + strengthBars: { + flexDirection: "row", + gap: 5, + }, + + strengthBar: { + flex: 1, + height: 7, + borderRadius: 4, + backgroundColor: "#ddd", + }, + + strengthBarActive: { + backgroundColor: "#236636", + }, + + strengthLabels: { + flexDirection: "row", + justifyContent: "space-between", + opacity: 0.6, + }, +}); diff --git a/src/utils/generating.ts b/src/utils/generating.ts index a928d336..4cd1135e 100644 --- a/src/utils/generating.ts +++ b/src/utils/generating.ts @@ -1,68 +1,130 @@ -interface PasswordOptions { +export const MIN_PASSWORD_LENGTH = 8; +export const MAX_PASSWORD_LENGTH = 64; + +const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +const LOWERCASE = "abcdefghijklmnopqrstuvwxyz"; +const NUMBERS = "0123456789"; +const SYMBOLS = "!@#$%^&*()-_=+[]{};:,.<>?"; + +export type PasswordGeneratorOptions = { length: number; - letters?: boolean; // A-Z + a-z (one option) - numbers?: boolean; // 0-9 - symbols?: boolean; // !@#$%^&* etc. -} + uppercase: boolean; + lowercase: boolean; + numbers: boolean; + symbols: boolean; +}; -export function generatePassword(options: PasswordOptions): string { - // Default: everything enabled - const { length, letters = true, numbers = true, symbols = true } = options; +export type PasswordStrength = { + score: number; + label: "Weak" | "Medium" | "Strong"; +}; - const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - const lower = "abcdefghijklmnopqrstuvwxyz"; - const nums = "0123456789"; - const syms = "!@#$%^&*()_+-=[]{}|;:,.<>?"; +export type CharacterType = keyof Omit; - let allChars = ""; - const guaranteed: string[] = []; +function randomCharacter(characters: string): string { + return characters[Math.floor(Math.random() * characters.length)]; +} + +export function generatePassword(options: PasswordGeneratorOptions): string { + const characterSets: string[] = []; + + if (options.uppercase) { + characterSets.push(UPPERCASE); + } - if (letters) { - allChars += upper + lower; - // Guarantee at least one upper and one lower - guaranteed.push(upper[secureRandom(upper.length)]); - guaranteed.push(lower[secureRandom(lower.length)]); + if (options.lowercase) { + characterSets.push(LOWERCASE); } - // Numbers - if (numbers) { - allChars += nums; - guaranteed.push(nums[secureRandom(nums.length)]); + if (options.numbers) { + characterSets.push(NUMBERS); } - // Symbols - if (symbols) { - allChars += syms; - guaranteed.push(syms[secureRandom(syms.length)]); + if (options.symbols) { + characterSets.push(SYMBOLS); } - // Validation - if (!allChars) { - throw new Error("Please select at least one character type."); + if (characterSets.length === 0) { + return ""; } - if (guaranteed.length > length) { - throw new Error("Too many required character types for the fixed length."); + const allCharacters = characterSets.join(""); + + const characters: string[] = []; + + // Guarantee that every selected character type + // appears at least once. + for (const characterSet of characterSets) { + characters.push(randomCharacter(characterSet)); } - // Fill remaining characters - const passwordChars = [...guaranteed]; - while (passwordChars.length < length) { - passwordChars.push(allChars[secureRandom(allChars.length)]); + while (characters.length < options.length) { + characters.push(randomCharacter(allCharacters)); } - // Shuffle so guaranteed characters are not always at the start - for (let i = passwordChars.length - 1; i > 0; i--) { - const j = secureRandom(i + 1); - [passwordChars[i], passwordChars[j]] = [passwordChars[j], passwordChars[i]]; + // Fisher-Yates shuffle. + for (let i = characters.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + + [characters[i], characters[j]] = [characters[j], characters[i]]; } - return passwordChars.join(""); + return characters.join(""); } -// Cryptographically secure random number -function secureRandom(max: number): number { - const array = new Uint32Array(1); - crypto.getRandomValues(array); - return array[0] % max; +export function getPasswordStrength( + options: PasswordGeneratorOptions, +): PasswordStrength { + let score = 0; + + if (options.length >= 12) { + score++; + } + + if (options.length >= 20) { + score++; + } + + if (options.uppercase) { + score++; + } + + if (options.lowercase) { + score++; + } + + if (options.numbers) { + score++; + } + + if (options.symbols) { + score++; + } + + if (score <= 2) { + return { + score, + label: "Weak", + }; + } + + if (score <= 4) { + return { + score, + label: "Medium", + }; + } + + return { + score, + label: "Strong", + }; } + +export const DEFAULT_PASSWORD_OPTIONS: PasswordGeneratorOptions = { + length: 20, + uppercase: true, + lowercase: true, + numbers: true, + symbols: true, +};