import 'package:flutter/material.dart'; import 'package:fancy_password_field/fancy_password_field.dart'; import 'package:pweb/config/constants.dart'; import 'package:pweb/models/state/control_state.dart'; import 'package:pweb/widgets/password/password.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; class PasswordUiController extends StatefulWidget { final TextEditingController controller; final String? labelText; final ControlState state; const PasswordUiController({ required this.controller, this.labelText, this.state = ControlState.enabled, super.key, }); @override State createState() => _PasswordUiControllerState(); } class _PasswordUiControllerState extends State { @override void initState() { super.initState(); widget.controller.addListener(_onPasswordChanged); } @override void dispose() { widget.controller.removeListener(_onPasswordChanged); super.dispose(); } void _onPasswordChanged() => setState(() {}); @override Widget build(BuildContext context) { final isEnabled = widget.state == ControlState.enabled; final specialRule = _SpecialCharacterValidationRule( customText: AppLocalizations.of( context, )!.passwordValidationRuleSpecialCharacter, ); final value = widget.controller.text; final missing = _allRules(context, specialRule) .where((rule) => !rule.validate(value)) .map((rule) => rule.name) .toList(growable: false); final hasMissingRules = value.isNotEmpty && missing.isNotEmpty; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ IgnorePointer( ignoring: !isEnabled, child: Opacity( opacity: isEnabled ? 1 : 0.6, child: Theme( data: hasMissingRules ? _invalidTheme(context) : Theme.of(context), child: defaulRulesPasswordField( context, controller: widget.controller, labelText: widget.labelText, additionalRules: {specialRule}, validationRuleBuilder: (_, _) => const SizedBox.shrink(), ), ), ), ), if (hasMissingRules) ...[ const SizedBox(height: 8), ...missing.map( (ruleText) => Padding( padding: const EdgeInsets.only(bottom: 2), child: Text( '• $ruleText', style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), ), ], ], ); } List _allRules( BuildContext context, ValidationRule specialRule, ) { final loc = AppLocalizations.of(context)!; return [ DigitValidationRule(customText: loc.passwordValidationRuleDigit), UppercaseValidationRule(customText: loc.passwordValidationRuleUpperCase), LowercaseValidationRule(customText: loc.passwordValidationRuleLowerCase), MinCharactersValidationRule( Constants.minPasswordCharacters, customText: loc.passwordValidationRuleMinCharacters( Constants.minPasswordCharacters, ), ), specialRule, ]; } ThemeData _invalidTheme(BuildContext context) { final theme = Theme.of(context); final errorColor = theme.colorScheme.error; final border = OutlineInputBorder( borderRadius: BorderRadius.circular(4), borderSide: BorderSide(color: errorColor, width: 1.2), ); return theme.copyWith( inputDecorationTheme: theme.inputDecorationTheme.copyWith( enabledBorder: border, focusedBorder: border, errorBorder: border, focusedErrorBorder: border, ), ); } } class _SpecialCharacterValidationRule extends ValidationRule { final String customText; _SpecialCharacterValidationRule({required this.customText}); @override String get name => customText; @override bool get showName => true; @override bool validate(String value) => value.runes.any(_isAsciiSpecialCharacter); bool _isAsciiSpecialCharacter(int code) => (code >= 33 && code <= 47) || (code >= 58 && code <= 64) || (code >= 91 && code <= 96) || (code >= 123 && code <= 126); }