1 import { useCallback, useEffect, useState } from 'react';
3 import { usePassCore } from '@proton/pass/components/Core/PassCoreProvider';
5 DEFAULT_MEMORABLE_PW_OPTIONS,
6 DEFAULT_RANDOM_PW_OPTIONS,
9 } from '@proton/pass/lib/password/constants';
10 import { generatePassword } from '@proton/pass/lib/password/generator';
11 import type { GeneratePasswordConfig, GeneratePasswordMode } from '@proton/pass/lib/password/types';
12 import type { MaybeNull } from '@proton/pass/types';
13 import { merge } from '@proton/pass/utils/object/merge';
14 import debounce from '@proton/utils/debounce';
15 import noop from '@proton/utils/noop';
17 export enum CharType {
23 /* Designers mixed the colors of different ui-${type}
24 * sub-themes for the character colors.. */
25 export const charTypeToClassName = {
26 [CharType.Alphabetic]: '',
27 [CharType.Digit]: 'ui-violet pass-password-generator--char-digit',
28 [CharType.Special]: 'ui-teal pass-password-generator--char-special',
31 export const getTypeFromChar = (char: string) => {
32 if (alphabeticChars.includes(char)) return CharType.Alphabetic;
33 if (digitChars.includes(char)) return CharType.Digit;
35 return CharType.Special;
38 export const getCharsGroupedByColor = (password: string) => {
39 if (password.length === 0) return [];
41 const [head, ...chars] = Array.from(password);
42 const startType = getTypeFromChar(head);
46 (state, currentChar) => {
47 const currentElement = state[state.length - 1];
48 const previousType = currentElement.color;
49 const currentType = getTypeFromChar(currentChar);
51 return previousType !== currentType
52 ? [...state, { color: currentType, content: currentChar }]
53 : [...state.slice(0, -1), { color: previousType, content: currentElement.content + currentChar }];
55 [{ color: startType, content: head }]
57 .map(({ color, content }, index) => (
58 <span className={charTypeToClassName[color]} key={index}>
64 type UsePasswordGeneratorOptions = {
65 initial: MaybeNull<GeneratePasswordConfig>;
66 onConfigChange: (options: GeneratePasswordConfig) => void;
69 export const usePasswordGenerator = ({ initial, onConfigChange }: UsePasswordGeneratorOptions) => {
70 const { core } = usePassCore();
71 const [config, setConfig] = useState<GeneratePasswordConfig>(initial ?? DEFAULT_MEMORABLE_PW_OPTIONS);
72 const generator = useCallback(generatePassword(core), [core]);
73 const [password, setPassword] = useState('');
75 const regeneratePassword = useCallback(() => {
76 generator(config).then(setPassword).catch(noop);
77 }, [generator, config]);
79 useEffect(regeneratePassword, []);
81 /** debounce the pw options dispatch in order to avoid swarming the
82 * store with updates when using the length slider */
83 const savePasswordOptions = useCallback(debounce(onConfigChange, 250), []);
85 const setPasswordOptions = <T extends GeneratePasswordConfig['type']>(
87 update?: Partial<Extract<GeneratePasswordConfig, { type: T }>['options']>
89 setConfig((options) => {
90 const newOptions = (() => {
91 if (update) return merge(options, { options: update });
92 if (type === 'memorable') return DEFAULT_MEMORABLE_PW_OPTIONS;
93 if (type === 'random') return DEFAULT_RANDOM_PW_OPTIONS;
97 savePasswordOptions(newOptions);
102 /* regenerate the password on each options change */
103 useEffect(() => regeneratePassword(), Object.values(config.options));
114 export type UsePasswordGeneratorResult<T extends GeneratePasswordMode = GeneratePasswordMode> = Omit<
115 ReturnType<typeof usePasswordGenerator>,
117 > & { config: GeneratePasswordConfig<T> };
119 export const isUsingRandomPassword = (
120 result: UsePasswordGeneratorResult
121 ): result is UsePasswordGeneratorResult<'random'> => result.config.type === 'random';
123 export const isUsingMemorablePassword = (
124 result: UsePasswordGeneratorResult
125 ): result is UsePasswordGeneratorResult<'memorable'> => result.config.type === 'memorable';