diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx index 2f5064447e..b420ed0872 100644 --- a/src/components/views/auth/PassphraseField.tsx +++ b/src/components/views/auth/PassphraseField.tsx @@ -40,11 +40,7 @@ interface IProps { onValidate(result: IValidationResult); } -interface IState { - complexity: zxcvbn.ZXCVBNResult; -} - -class PassphraseField extends PureComponent { +class PassphraseField extends PureComponent { static defaultProps = { label: _td("Password"), labelEnterPassword: _td("Enter password"), @@ -52,14 +48,16 @@ class PassphraseField extends PureComponent { labelAllowedButUnsafe: _td("Password is allowed, but unsafe"), }; - state = { complexity: null }; - - public readonly validate = withValidation({ - description: function() { - const complexity = this.state.complexity; + public readonly validate = withValidation({ + description: function(complexity) { const score = complexity ? complexity.score : 0; return ; }, + deriveData: async ({ value }) => { + if (!value) return null; + const { scorePassword } = await import('../../../utils/PasswordScorer'); + return scorePassword(value); + }, rules: [ { key: "required", @@ -68,28 +66,24 @@ class PassphraseField extends PureComponent { }, { key: "complexity", - test: async function({ value }) { + test: async function({ value }, complexity) { if (!value) { return false; } - const { scorePassword } = await import('../../../utils/PasswordScorer'); - const complexity = scorePassword(value); - this.setState({ complexity }); const safe = complexity.score >= this.props.minScore; const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"]; return allowUnsafe || safe; }, - valid: function() { + valid: function(complexity) { // Unsafe passwords that are valid are only possible through a // configuration flag. We'll print some helper text to signal // to the user that their password is allowed, but unsafe. - if (this.state.complexity.score >= this.props.minScore) { + if (complexity.score >= this.props.minScore) { return _t(this.props.labelStrongPassword); } return _t(this.props.labelAllowedButUnsafe); }, - invalid: function() { - const complexity = this.state.complexity; + invalid: function(complexity) { if (!complexity) { return null; } diff --git a/src/components/views/elements/Validation.tsx b/src/components/views/elements/Validation.tsx index 50544c9f51..55e5714719 100644 --- a/src/components/views/elements/Validation.tsx +++ b/src/components/views/elements/Validation.tsx @@ -21,18 +21,19 @@ import classNames from "classnames"; type Data = Pick; -interface IRule { +interface IRule { key: string; final?: boolean; - skip?(this: T, data: Data): boolean; - test(this: T, data: Data): boolean | Promise; - valid?(this: T): string; - invalid?(this: T): string; + skip?(this: T, data: Data, derivedData: D): boolean; + test(this: T, data: Data, derivedData: D): boolean | Promise; + valid?(this: T, derivedData: D): string; + invalid?(this: T, derivedData: D): string; } -interface IArgs { - rules: IRule[]; - description(this: T): React.ReactChild; +interface IArgs { + rules: IRule[]; + description(this: T, derivedData: D): React.ReactChild; + deriveData?(data: Data): Promise; } export interface IFieldState { @@ -53,6 +54,10 @@ export interface IValidationResult { * @param {Function} description * Function that returns a string summary of the kind of value that will * meet the validation rules. Shown at the top of the validation feedback. + * @param {Function} deriveData + * Optional function that returns a Promise to an object of generic type D. + * The result of this Promise is passed to rule methods `skip`, `test`, `valid`, and `invalid`. + * Useful for doing calculations per-value update once rather than in each of the above rule methods. * @param {Object} rules * An array of rules describing how to check to input value. Each rule in an object * and may have the following properties: @@ -66,7 +71,7 @@ export interface IValidationResult { * A validation function that takes in the current input value and returns * the overall validity and a feedback UI that can be rendered for more detail. */ -export default function withValidation({ description, rules }: IArgs) { +export default function withValidation({ description, deriveData, rules }: IArgs) { return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise { if (!value && allowEmpty) { return { @@ -75,6 +80,9 @@ export default function withValidation({ description, rules }: IA }; } + const data = { value, allowEmpty }; + const derivedData = deriveData ? await deriveData(data) : undefined; + const results = []; let valid = true; if (rules && rules.length) { @@ -87,20 +95,18 @@ export default function withValidation({ description, rules }: IA continue; } - const data = { value, allowEmpty }; - - if (rule.skip && rule.skip.call(this, data)) { + if (rule.skip && rule.skip.call(this, data, derivedData)) { continue; } // We're setting `this` to whichever component holds the validation // function. That allows rules to access the state of the component. - const ruleValid = await rule.test.call(this, data); + const ruleValid = await rule.test.call(this, data, derivedData); valid = valid && ruleValid; if (ruleValid && rule.valid) { // If the rule's result is valid and has text to show for // the valid state, show it. - const text = rule.valid.call(this); + const text = rule.valid.call(this, derivedData); if (!text) { continue; } @@ -112,7 +118,7 @@ export default function withValidation({ description, rules }: IA } else if (!ruleValid && rule.invalid) { // If the rule's result is invalid and has text to show for // the invalid state, show it. - const text = rule.invalid.call(this); + const text = rule.invalid.call(this, derivedData); if (!text) { continue; } @@ -153,7 +159,7 @@ export default function withValidation({ description, rules }: IA if (description) { // We're setting `this` to whichever component holds the validation // function. That allows rules to access the state of the component. - const content = description.call(this); + const content = description.call(this, derivedData); summary =
{content}
; }