From ab02bf233f560d35184b146803351a03cf4ac9a8 Mon Sep 17 00:00:00 2001 From: GittHub-d Date: Mon, 10 Feb 2025 14:02:25 +0200 Subject: [PATCH] [MDS-1595] Auth code component --- .../stories/primitives/auth_code.dart | 52 +- lib/src/widgets/auth_code/auth_code.dart | 871 +++++------------- 2 files changed, 247 insertions(+), 676 deletions(-) diff --git a/example/lib/src/storybook/stories/primitives/auth_code.dart b/example/lib/src/storybook/stories/primitives/auth_code.dart index 474ca4cd..b701f515 100644 --- a/example/lib/src/storybook/stories/primitives/auth_code.dart +++ b/example/lib/src/storybook/stories/primitives/auth_code.dart @@ -1,6 +1,7 @@ import 'package:example/src/storybook/common/color_options.dart'; import 'package:example/src/storybook/common/widgets/text_divider.dart'; import 'package:flutter/material.dart'; +import 'package:moon_core/moon_core.dart'; import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; @@ -27,7 +28,7 @@ class AuthCodeStory extends StatelessWidget { ); final shapeKnob = context.knobs.nullable.options( - label: "shape", + label: "authFieldShape", description: "Shape of MoonAuthCode input fields.", enabled: false, initial: AuthFieldShape.box, @@ -50,7 +51,7 @@ class AuthCodeStory extends StatelessWidget { final textColor = colorTable(context)[textColorKnob ?? 40]; final cursorColorKnob = context.knobs.nullable.options( - label: "authFieldCursorColor", + label: "cursorColor", description: "MoonColors variants for MoonAuthCode cursor.", enabled: false, initial: 0, @@ -60,6 +61,18 @@ class AuthCodeStory extends StatelessWidget { final cursorColor = colorTable(context)[cursorColorKnob ?? 40]; + final cursorErrorColorKnob = context.knobs.nullable.options( + label: "cursorErrorColor", + description: + "MoonColors variants for MoonAuthCode cursor in error state.", + enabled: false, + initial: 0, + // piccolo + options: colorOptions, + ); + + final cursorErrorColor = colorTable(context)[cursorErrorColorKnob ?? 40]; + final selectedFillColorKnob = context.knobs.nullable.options( label: "selectedFillColor", description: "MoonColors variants for MoonAuthCode selected input field.", @@ -144,7 +157,7 @@ class AuthCodeStory extends StatelessWidget { description: "Gap between MoonAuthCode input fields.", enabled: false, initial: 8, - max: 12, + max: 16, ); final enableKnob = context.knobs.boolean( @@ -163,7 +176,7 @@ class AuthCodeStory extends StatelessWidget { ); final errorAnimationKnob = context.knobs.boolean( - label: "Error shake animation", + label: "errorAnimationType", description: "Show error with shake animation (ErrorAnimationType.shake).", ); @@ -189,7 +202,8 @@ class AuthCodeStory extends StatelessWidget { mainAxisAlignmentKnob ?? MainAxisAlignment.center, borderRadius: borderRadius, textStyle: TextStyle(color: textColor), - authFieldCursorColor: cursorColor, + cursorColor: cursorColor, + cursorErrorColor: cursorErrorColor, selectedFillColor: selectedFillColor, activeFillColor: activeFillColor, inactiveFillColor: inactiveFillColor, @@ -202,7 +216,7 @@ class AuthCodeStory extends StatelessWidget { peekWhenObscuring: peekWhenObscuringKnob, validator: (String? value) => null, errorBuilder: (BuildContext context, String? errorText) => - const SizedBox(), + const SizedBox.shrink(), ), const TextDivider(text: "Active MoonAuthCode"), MoonAuthCode( @@ -212,7 +226,8 @@ class AuthCodeStory extends StatelessWidget { mainAxisAlignmentKnob ?? MainAxisAlignment.center, borderRadius: borderRadius, textStyle: TextStyle(color: textColor), - authFieldCursorColor: cursorColor, + cursorColor: cursorColor, + cursorErrorColor: cursorErrorColor, selectedFillColor: selectedFillColor, activeFillColor: activeFillColor, inactiveFillColor: inactiveFillColor, @@ -224,8 +239,8 @@ class AuthCodeStory extends StatelessWidget { obscureText: obscuringKnob, peekWhenObscuring: peekWhenObscuringKnob, validator: (String? value) => null, - errorBuilder: (BuildContext context, String? errorText) => - const SizedBox(), + errorBuilder: (BuildContext _, String? __) => + const SizedBox.shrink(), ), const TextDivider(text: "Error MoonAuthCode"), SizedBox( @@ -233,6 +248,7 @@ class AuthCodeStory extends StatelessWidget { child: MoonAuthCode( enableInputFill: true, authInputFieldCount: 4, + disabledOpacityValue: 1, mainAxisAlignment: mainAxisAlignmentKnob ?? MainAxisAlignment.center, errorAnimationType: errorAnimationKnob @@ -240,28 +256,28 @@ class AuthCodeStory extends StatelessWidget { : ErrorAnimationType.noAnimation, borderRadius: borderRadius, textStyle: TextStyle(color: textColor), - authFieldCursorColor: cursorColor, + cursorColor: cursorColor, + cursorErrorColor: cursorErrorColor, selectedFillColor: selectedFillColor, activeFillColor: activeFillColor, inactiveFillColor: inactiveFillColor, selectedBorderColor: selectedBorderColor, activeBorderColor: activeBorderColor, inactiveBorderColor: inactiveBorderColor, + animationDuration: const Duration(seconds: 1), gap: gapKnob?.toDouble(), authFieldShape: shapeKnob, obscureText: obscuringKnob, peekWhenObscuring: peekWhenObscuringKnob, validator: (String? pin) { - return (pin != null && pin != '0000' && pin.length == 4) - ? 'The input must be exactly "0000".' + return (pin != null && pin != "0000" && pin.length == 4) + ? "The input must be exactly '0000'." : null; }, - errorBuilder: (BuildContext context, String? errorText) { - return Align( - child: Padding( - padding: const EdgeInsets.only(top: 8), - child: Text(errorText ?? ''), - ), + errorBuilder: (BuildContext _, String? errorText) { + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Text(errorText ?? ""), ); }, ), diff --git a/lib/src/widgets/auth_code/auth_code.dart b/lib/src/widgets/auth_code/auth_code.dart index ec577528..45b3ba23 100644 --- a/lib/src/widgets/auth_code/auth_code.dart +++ b/lib/src/widgets/auth_code/auth_code.dart @@ -1,19 +1,14 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:mix/mix.dart'; import 'package:moon_core/moon_core.dart'; -import 'package:moon_design/src/theme/theme.dart'; import 'package:moon_design/src/theme/tokens/borders.dart'; import 'package:moon_design/src/theme/tokens/opacities.dart'; import 'package:moon_design/src/theme/tokens/sizes.dart'; import 'package:moon_design/src/theme/tokens/transitions.dart'; import 'package:moon_design/src/theme/tokens/typography/typography.dart'; -import 'package:moon_design/src/utils/extensions.dart'; -import 'package:moon_design/src/utils/squircle/squircle_border.dart'; import 'package:moon_tokens/moon_tokens.dart'; @@ -23,17 +18,15 @@ enum AuthFieldShape { circle, } -enum ErrorAnimationType { - noAnimation, - shake, +enum _InputFieldState { + active, + inactive, + selected, + error, + selectedAndError, } -typedef MoonAuthCodeErrorBuilder = Widget Function( - BuildContext context, - String? errorText, -); - -class MoonAuthCode extends StatefulWidget { +class MoonAuthCode extends StatelessWidget { /// The shape of the auth code input field. final AuthFieldShape? authFieldShape; @@ -62,7 +55,7 @@ class MoonAuthCode extends StatefulWidget { final bool peekWhenObscuring; /// Whether to show the cursor in the selected auth code input field. - final bool showAuthFieldCursor; + final bool showCursor; /// Whether to use haptic feedback (vibration) for auth code error state. final bool useHapticFeedback; @@ -70,10 +63,13 @@ class MoonAuthCode extends StatefulWidget { /// The border radius of the auth code input field. final BorderRadiusGeometry? borderRadius; - /// The cursor color of the auth code input field. - final Color? authFieldCursorColor; + /// The cursor color of the selected auth code input field. + final Color? cursorColor; + + /// The cursor color of the selected auth code input field in error state. + final Color? cursorErrorColor; - /// The border color of the selected auth code input field. + /// The border color of the currently selected auth code input field. final Color? selectedBorderColor; /// The border color of the auth code input field with input. @@ -115,22 +111,26 @@ class MoonAuthCode extends StatefulWidget { /// The width of the auth code input field. final double? width; - /// The duration of the auth code input field transition animation. + /// The duration of the transition animation when the auth code input field + /// switches between enabled and disabled states. final Duration? animationDuration; - /// The duration of the auth code error state animation. + /// The curve of the transition animation when the auth code input field + /// switches between enabled and disabled states. + final Curve? animationCurve; + + /// The duration of the auth code error state animation, + /// if [ErrorState.shake] is selected. final Duration? errorAnimationDuration; + /// The curve of the auth code error state animation + /// if [ErrorState.shake] is selected. + final Curve? errorAnimationCurve; + /// The duration to display the typed character before it is obscured with /// [obscuringCharacter]. The [peekWhenObscuring] has to be set to true. final Duration? peekDuration; - /// The curve of the auth code input field transition animation. - final Curve? animationCurve; - - /// The curve of the auth code error state animation. - final Curve? errorAnimationCurve; - /// The animation type for the auth code validation error. final ErrorAnimationType errorAnimationType; @@ -141,12 +141,15 @@ class MoonAuthCode extends StatefulWidget { final int authInputFieldCount; /// The list of shadows applied to the auth code input field. + /// For finer control, use [activeBoxShadows] and [inActiveBoxShadows]. final List? boxShadows; /// The list of shadows applied to the auth code input field with input. + /// Takes precedence over [boxShadows]. final List? activeBoxShadows; /// The list of shadows applied to the auth code input field without input. + /// Takes precedence over [boxShadows]. final List? inActiveBoxShadows; /// {@macro flutter.widgets.editableText.inputFormatters} @@ -161,10 +164,6 @@ class MoonAuthCode extends StatefulWidget { /// The validator errors take precedence over the provided [errorText]. final String? errorText; - /// The character or placeholder to display in the auth code input field when - /// its value is empty. - final String? hintCharacter; - /// The character to use to obscure the text when [obscureText] is true. /// /// Defaults to Unicode character U+2022 BULLET (•). @@ -179,9 +178,6 @@ class MoonAuthCode extends StatefulWidget { /// The keyboard [TextInputType] for the auth code. final TextInputType keyboardType; - /// The text style of the [hintCharacter]. - final TextStyle? hintStyle; - /// The text style of the auth code. final TextStyle? textStyle; @@ -218,6 +214,14 @@ class MoonAuthCode extends StatefulWidget { /// A builder to build the auth code error widget. final MoonAuthCodeErrorBuilder errorBuilder; + /// The helper text to display below the auth code when provided. + /// Auth code helper text is not be visible in error state. + final Widget? helperText; + + /// The character or placeholder to display in the auth code input field when + /// its value is empty. + final Widget? hint; + /// The widget to obscure the auth code input field text. /// /// Overrides the [obscuringCharacter]. @@ -234,10 +238,11 @@ class MoonAuthCode extends StatefulWidget { this.enableInputFill = false, this.obscureText = false, this.peekWhenObscuring = false, - this.showAuthFieldCursor = true, + this.showCursor = true, this.useHapticFeedback = false, this.borderRadius, - this.authFieldCursorColor, + this.cursorColor, + this.cursorErrorColor, this.selectedBorderColor, this.activeBorderColor, this.inactiveBorderColor, @@ -263,13 +268,12 @@ class MoonAuthCode extends StatefulWidget { this.inActiveBoxShadows, this.inputFormatters, this.mainAxisAlignment = MainAxisAlignment.center, - this.hintCharacter, + this.hint, this.obscuringCharacter = '•', this.semanticLabel, this.textInputAction = TextInputAction.done, this.keyboardType = TextInputType.visiblePassword, this.errorText, - this.hintStyle, this.textStyle, this.errorTextStyle, this.textController, @@ -279,663 +283,214 @@ class MoonAuthCode extends StatefulWidget { this.onSubmitted, this.onEditingComplete, required this.errorBuilder, + this.helperText, this.obscuringWidget, }) : assert(authInputFieldCount > 0), assert(height == null || height > 0), assert(width == null || width > 0); @override - _MoonAuthCodeState createState() => _MoonAuthCodeState(); -} - -class _MoonAuthCodeState extends State - with TickerProviderStateMixin { - late FocusNode _focusNode; - late List _inputList; - - late BorderRadiusGeometry _effectiveBorderRadius; - late Color _effectiveSelectedBorderColor; - late Color _effectiveActiveBorderColor; - late Color _effectiveInactiveBorderColor; - late Color _effectiveErrorBorderColor; - late Color _effectiveSelectedFillColor; - late Color _effectiveActiveFillColor; - late Color _effectiveInactiveFillColor; - late Color _effectiveTextColor; - late Color _effectiveCursorColor; - late double _effectiveBorderWidth; - late double _effectiveGap; - late double _effectiveHeight; - late double _effectiveWidth; - late TextStyle _effectiveTextStyle; - late TextStyle _effectiveErrorTextStyle; - - late TextEditingController _textEditingController; - late AnimationController _cursorController; - late Animation _cursorAnimation; - - AnimationController? _errorAnimationController; - Animation? _errorOffsetAnimation; - - Duration? _peekDuration; - Duration? _animationDuration; - Curve? _animationCurve; - - bool _isInErrorMode = false; - bool _hasPeeked = false; - int _selectedIndex = 0; - Timer? _peekDebounce; - - TextStyle get _resolvedTextStyle => - _effectiveTextStyle.merge(widget.textStyle).copyWith( - color: _isInErrorMode - ? _resolvedErrorTextStyle.color - : widget.textStyle?.color ?? _effectiveTextColor, - ); - - TextStyle get _hintStyle => _resolvedTextStyle.merge(widget.hintStyle); - - TextStyle get _resolvedErrorTextStyle => - _effectiveErrorTextStyle.merge(widget.errorTextStyle).copyWith( - color: widget.errorTextStyle?.color ?? _effectiveErrorBorderColor, - ); - - Color get _resolvedErrorCursorColor => _isInErrorMode - ? _resolvedErrorTextStyle.color ?? _effectiveErrorBorderColor - : _effectiveCursorColor; - - void _initializeFields() { - _initializeFocusNode(); - _initializeInputList(); - _initializeErrorAnimationListener(); - _initializeTextEditingController(); - _initializeAuthFieldCursor(); - } - - void _initializeFocusNode() { - _focusNode = (widget.focusNode ?? FocusNode()) - ..addListener(() => setState(() {})); - } - - void _initializeInputList() { - _inputList = List.filled(widget.authInputFieldCount, ''); - } - - void _initializeTextEditingController() { - _textEditingController = widget.textController ?? TextEditingController(); - - _textEditingController.addListener(() { - // Since we use custom error builder, manual input validation is required - // to trigger validation error. The _validateInput() method returns an - // error String in case of an validation error, otherwise null. - if (_validateInput() != null) { - if (widget.errorAnimationType == ErrorAnimationType.shake) { - _errorAnimationController!.forward(); - - if (widget.useHapticFeedback) HapticFeedback.lightImpact(); - } - if (!_isInErrorMode) _setState(() => _isInErrorMode = true); - } else { - if (_isInErrorMode && widget.errorText == null) { - _setState(() => _isInErrorMode = false); - } - } - - _debounceBlink(); - - String currentText = _textEditingController.text; - if (widget.enabled && _inputList.join() != currentText) { - if (currentText.length >= widget.authInputFieldCount) { - if (widget.onCompleted != null) { - if (currentText.length > widget.authInputFieldCount) { - currentText = - currentText.substring(0, widget.authInputFieldCount); - } - Future.delayed( - const Duration(milliseconds: 100), - () => widget.onCompleted!(currentText), - ); - } - if (widget.autoDismissKeyboard) _focusNode.unfocus(); - } - widget.onChanged?.call(currentText); - } - - _updateTextField(currentText); - }); - - // If a default value is set for the TextEditingController, - // update the text field initial value accordingly. - if (_textEditingController.text.isNotEmpty) { - _updateTextField(_textEditingController.text); - } - } - - void _initializeAuthFieldCursor() { - _cursorController = - AnimationController(duration: const Duration(seconds: 1), vsync: this); - _cursorAnimation = Tween(begin: 1, end: 0).animate( - CurvedAnimation( - parent: _cursorController, - curve: Curves.easeInOut, - ), - ); - - if (widget.showAuthFieldCursor) _cursorController.repeat(); - } - - void _initializeErrorAnimationListener() { - WidgetsBinding.instance.addPostFrameCallback((Duration _) { - if (!mounted) return; - - _errorAnimationController!.addStatusListener((AnimationStatus status) { - if (status == AnimationStatus.completed) { - _errorAnimationController!.reverse(); - } - }); - }); - } - - void _debounceBlink() { - _hasPeeked = true; - - if (widget.peekWhenObscuring && - _textEditingController.text.length > - _inputList.where((x) => x.isNotEmpty).length) { - _setState(() => _hasPeeked = false); - - if (_peekDebounce?.isActive ?? false) _peekDebounce!.cancel(); - - _peekDebounce = Timer(_peekDuration!, () { - _setState(() => _hasPeeked = true); - }); - } - } - - void _onFocus() { - if (widget.autoUnfocus) { - if (_focusNode.hasFocus && - MediaQuery.of(context).viewInsets.bottom == 0) { - _focusNode.unfocus(); - Future.delayed( - const Duration(microseconds: 1), - () => _focusNode.requestFocus(), - ); - } else { - _focusNode.requestFocus(); - } - } else { - _focusNode.requestFocus(); - } - } - - Color _getBorderColorFromIndex(int index) { - if (((_selectedIndex == index) || - (_selectedIndex == index + 1 && - index + 1 == widget.authInputFieldCount)) && - _focusNode.hasFocus) { - return _isInErrorMode - ? _effectiveErrorBorderColor - : _effectiveSelectedBorderColor; - } else if (_selectedIndex > index) { - return _isInErrorMode - ? _effectiveErrorBorderColor - : _effectiveActiveBorderColor; - } - - return _isInErrorMode - ? _effectiveErrorBorderColor - : _effectiveInactiveBorderColor; - } - - Color _getFillColorFromIndex(int index) { - if (((_selectedIndex == index) || - (_selectedIndex == index + 1 && - index + 1 == widget.authInputFieldCount)) && - _focusNode.hasFocus) { - return _effectiveSelectedFillColor; - } else if (_selectedIndex > index) { - return _effectiveActiveFillColor; - } - - return _effectiveInactiveFillColor; - } - - double _getBorderWidthFromIndex(int index) { - if (((_selectedIndex == index) || - (_selectedIndex == index + 1 && - index + 1 == widget.authInputFieldCount)) && - _focusNode.hasFocus) { - return _effectiveBorderWidth + 1; - } - - return _effectiveBorderWidth; - } - - List? _getBoxShadowFromIndex(int index) { - if (_selectedIndex == index) { - return widget.activeBoxShadows; - } else if (_selectedIndex > index) { - return widget.inActiveBoxShadows; - } - - return []; - } - - ShapeBorder _getAuthInputFieldShape({required int elementIndex}) { - final BorderSide borderSide = BorderSide( - color: _getBorderColorFromIndex(elementIndex), - width: _getBorderWidthFromIndex(elementIndex), - ); - - switch (widget.authFieldShape) { - case AuthFieldShape.circle: - return CircleBorder(side: borderSide); - case AuthFieldShape.underline: - return Border(bottom: borderSide); - default: - return MoonSquircleBorder( - borderRadius: _effectiveBorderRadius.squircleBorderRadius(context), - side: borderSide, - ); - } - } + Widget build(BuildContext context) { + final BorderRadiusGeometry effectiveBorderRadius = + borderRadius ?? MoonBorders.borders.interactiveSm; - Future _updateTextField(String text) async { - final List updatedList = - List.filled(widget.authInputFieldCount, ''); + final double effectiveBorderWidth = + borderWidth ?? MoonBorders.borders.defaultBorderWidth; - for (int i = 0; i < widget.authInputFieldCount; i++) { - updatedList[i] = text.length > i ? text[i] : ''; - } + final double effectiveGap = gap ?? MoonSizes.sizes.x4s; - _setState(() { - _selectedIndex = text.length; - _inputList = updatedList; - }); - } + final double effectiveHeight = height ?? MoonSizes.sizes.xl; - String? _validateInput() => - widget.validator.call(_textEditingController.text); + final double effectiveWidth = width ?? MoonSizes.sizes.lg; - void _setState(void Function() function) { - if (mounted) setState(function); - } + final Color effectiveSelectedBorderColor = + selectedBorderColor ?? MoonColors.light.piccolo; - @override - void initState() { - super.initState(); + final Color effectiveActiveBorderColor = + activeBorderColor ?? MoonColors.light.beerus; - _isInErrorMode = widget.errorText != null; + final Color effectiveInactiveBorderColor = + inactiveBorderColor ?? MoonColors.light.beerus; - _initializeFields(); - } + final Color effectiveErrorBorderColor = + errorBorderColor ?? MoonColors.light.chichi; - @override - void didUpdateWidget(MoonAuthCode oldWidget) { - super.didUpdateWidget(oldWidget); + final Color effectiveSelectedFillColor = + selectedFillColor ?? MoonColors.light.goku; - if (oldWidget.errorText != widget.errorText) { - _setState(() { - _isInErrorMode = widget.errorText != null || _validateInput() != null; - }); - } - } + final Color effectiveActiveFillColor = + activeFillColor ?? MoonColors.light.goku; - @override - void dispose() { - if (widget.textController == null) _textEditingController.dispose(); - if (widget.focusNode == null) _focusNode.dispose(); + final Color effectiveInactiveFillColor = + inactiveFillColor ?? MoonColors.light.goku; - _errorAnimationController!.dispose(); - _cursorController.dispose(); + final Color effectiveTextColor = + textStyle?.color ?? MoonColors.light.textPrimary; - super.dispose(); - } + final Color effectiveErrorColor = + errorTextStyle?.color ?? effectiveErrorBorderColor; - List _generateAuthInputFields() { - final List authInputFields = []; - - for (int i = 0; i < widget.authInputFieldCount; i++) { - authInputFields.add( - Padding( - padding: EdgeInsetsDirectional.only( - end: i == widget.authInputFieldCount - 1 ? 0 : _effectiveGap, - ), - child: RepaintBoundary( - child: Container( - width: _effectiveWidth, - height: _effectiveHeight, - decoration: ShapeDecorationWithPremultipliedAlpha( - shape: _getAuthInputFieldShape(elementIndex: i), - color: widget.enableInputFill - ? _getFillColorFromIndex(i) - : Colors.transparent, - shadows: (widget.activeBoxShadows != null || - widget.inActiveBoxShadows != null) - ? _getBoxShadowFromIndex(i) - : widget.boxShadows, - ), - child: Center( - child: _buildChild(i), - ), - ), - ), - ), - ); - } + final Duration effectiveAnimationDuration = animationDuration ?? + MoonTransitions.transitions.defaultTransitionDuration; - return authInputFields; - } + final Curve effectiveAnimationCurve = + animationCurve ?? MoonTransitions.transitions.defaultTransitionCurve; - Widget _buildChild(int index) { - if (((_selectedIndex == index) || - (_selectedIndex == index + 1 && - index + 1 == widget.authInputFieldCount)) && - _focusNode.hasFocus && - widget.showAuthFieldCursor) { - final double cursorHeight = _resolvedTextStyle.fontSize!; - - if (_selectedIndex == index + 1 && - index + 1 == widget.authInputFieldCount) { - return Stack( - alignment: Alignment.center, - children: [ - Center( - child: Padding( - padding: EdgeInsets.only( - left: _resolvedTextStyle.fontSize! / 1.5, - ), - child: FadeTransition( - opacity: _cursorAnimation, - child: CustomPaint( - size: Size(0, cursorHeight), - painter: _CursorPainter( - cursorColor: _resolvedErrorCursorColor, - ), - ), - ), - ), - ), - _renderAuthInputFieldText(index: index), - ], - ); - } else { - return Center( - child: FadeTransition( - opacity: _cursorAnimation, - child: CustomPaint( - size: Size(0, cursorHeight), - painter: _CursorPainter( - cursorColor: _resolvedErrorCursorColor, - ), - ), - ), - ); - } - } - return _renderAuthInputFieldText(index: index); - } + final Duration effectivePeekDuration = + peekDuration ?? MoonTransitions.transitions.defaultTransitionDuration; - Widget _renderAuthInputFieldText({@required int? index}) { - assert(index != null); + final double effectiveDisabledOpacityValue = + disabledOpacityValue ?? MoonOpacities.opacities.disabled; - final bool showObscured = !widget.peekWhenObscuring || - (widget.peekWhenObscuring && _hasPeeked) || - index != _inputList.where((x) => x.isNotEmpty).length - 1; + final Duration effectiveErrorAnimationDuration = errorAnimationDuration ?? + MoonTransitions.transitions.defaultTransitionDuration; - if (widget.obscuringWidget != null && - showObscured && - _inputList[index!].isNotEmpty) { - return widget.obscuringWidget!; - } + final Curve effectiveErrorAnimationCurve = errorAnimationCurve ?? + MoonTransitions.transitions.defaultTransitionCurve; - if (_inputList[index!].isEmpty && widget.hintCharacter != null) { - return Text( - widget.hintCharacter!, - key: ValueKey(_inputList[index]), - style: _hintStyle, + final TextStyle effectiveTextStyle = MoonTypography.typography.body.text24; + + final TextStyle effectiveErrorTextStyle = + MoonTypography.typography.body.text12; + + final TextStyle resolvedInputTextStyle = + effectiveTextStyle.merge(textStyle).copyWith(color: effectiveTextColor); + + final TextStyle resolvedErrorInputTextStyle = effectiveTextStyle + .merge(textStyle) + .copyWith(color: effectiveErrorColor); + + final TextStyle resolvedErrorTextStyle = effectiveErrorTextStyle + .merge(errorTextStyle) + .copyWith(color: effectiveErrorColor); + + final TextStyle resolvedHelperTextStyle = + resolvedErrorTextStyle.copyWith(color: effectiveTextColor); + + ShapeDecorationWithPremultipliedAlpha decoration(_InputFieldState state) { + final double borderWidth = switch (state) { + _InputFieldState.selected => effectiveBorderWidth + 1, + _InputFieldState.selectedAndError => effectiveBorderWidth + 1, + _ => effectiveBorderWidth + }; + + final Color borderColor = switch (state) { + _InputFieldState.active => effectiveActiveBorderColor, + _InputFieldState.inactive => effectiveInactiveBorderColor, + _InputFieldState.selected => effectiveSelectedBorderColor, + _ => effectiveErrorBorderColor, + }; + + final Color fillColor = switch (state) { + _InputFieldState.active => effectiveActiveFillColor, + _InputFieldState.inactive => effectiveInactiveFillColor, + _InputFieldState.error => effectiveActiveFillColor, + _ => effectiveSelectedFillColor, + }; + + final List? boxShadow = switch (state) { + _InputFieldState.inactive => inActiveBoxShadows ?? boxShadows, + _ => activeBoxShadows ?? boxShadows, + }; + + final BorderSide borderSide = + BorderSide(color: borderColor, width: borderWidth); + + final OutlinedBorder shape = switch (authFieldShape) { + AuthFieldShape.circle => CircleBorder(side: borderSide), + AuthFieldShape.underline => LinearBorder.bottom(side: borderSide), + _ => MoonBorder(borderRadius: effectiveBorderRadius, side: borderSide), + }; + + return ShapeDecorationWithPremultipliedAlpha( + color: enableInputFill ? fillColor : Colors.transparent, + shape: shape, + shadows: boxShadow, ); } - final String text = - widget.obscureText && _inputList[index].isNotEmpty && showObscured - ? widget.obscuringCharacter - : _inputList[index]; - - return Text( - text, - key: ValueKey(_inputList[index]), - style: _resolvedTextStyle, - ); - } - - Widget _getTextFormField() { - final List inputFormatters = [ - LengthLimitingTextInputFormatter(widget.authInputFieldCount), - ]; - - if (widget.inputFormatters != null) { - inputFormatters.addAll(widget.inputFormatters!); - } - - return Directionality( - textDirection: Directionality.of(context), - child: SizedBox( - height: _effectiveHeight, - child: TextFormField( - autocorrect: false, - autofocus: widget.autoFocus, - autovalidateMode: AutovalidateMode.onUserInteraction, - controller: _textEditingController, - cursorWidth: 0.01, - enabled: widget.enabled, - enableInteractiveSelection: false, - enableSuggestions: false, - focusNode: _focusNode, - inputFormatters: inputFormatters, - keyboardType: widget.keyboardType, - onChanged: widget.onChanged, - onEditingComplete: widget.onEditingComplete, - onFieldSubmitted: widget.onSubmitted, - obscureText: widget.obscureText, - obscuringCharacter: widget.obscuringCharacter, - scrollPadding: const EdgeInsets.all(24.0), - showCursor: true, - smartDashesType: SmartDashesType.disabled, - textInputAction: widget.textInputAction, - decoration: const InputDecoration( - contentPadding: EdgeInsets.zero, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - ), - style: const TextStyle( - color: Colors.transparent, - fontSize: kIsWeb ? 1 : 0.01, - height: .01, - ), - ), + final Style inputFieldStyle = Style( + $box.chain + ..height(effectiveHeight) + ..width(effectiveWidth) + ..shapeDecoration.as(decoration(_InputFieldState.inactive)), + $flex.chain + ..gap(effectiveGap) + ..mainAxisAlignment(mainAxisAlignment), + $text.color(effectiveTextColor), + $with.defaultTextStyle.style.as(resolvedInputTextStyle), + $with.animatedOpacity( + opacity: enabled ? 1 : effectiveDisabledOpacityValue, + duration: effectiveAnimationDuration, + curve: effectiveAnimationCurve, + ), + $on.active( + $box.shapeDecoration.as(decoration(_InputFieldState.active)), + ), + $on.selected( + $box.shapeDecoration.as(decoration(_InputFieldState.selected)), + ), + $on.error( + $box.shapeDecoration.as(decoration(_InputFieldState.error)), + $text.color(effectiveErrorColor), + $with.defaultTextStyle.style.as(resolvedErrorInputTextStyle), + $with.iconTheme.data.color(effectiveErrorColor), + ), + ($on.selected & $on.error)( + $box.shapeDecoration.as(decoration(_InputFieldState.selectedAndError)), ), ); - } - - @override - Widget build(BuildContext context) { - _effectiveBorderRadius = widget.borderRadius ?? - context.moonTheme?.authCodeTheme.properties.borderRadius ?? - MoonBorders.borders.interactiveSm; - - _effectiveBorderWidth = widget.borderWidth ?? - context.moonBorders?.defaultBorderWidth ?? - MoonBorders.borders.defaultBorderWidth; - - _effectiveGap = widget.gap ?? - context.moonTheme?.authCodeTheme.properties.gap ?? - MoonSizes.sizes.x4s; - _effectiveHeight = widget.height ?? - context.moonTheme?.authCodeTheme.properties.height ?? - MoonSizes.sizes.xl; - - _effectiveWidth = widget.width ?? - context.moonTheme?.authCodeTheme.properties.width ?? - MoonSizes.sizes.lg; - - _effectiveSelectedBorderColor = widget.selectedBorderColor ?? - context.moonTheme?.authCodeTheme.colors.selectedBorderColor ?? - MoonColors.light.piccolo; - - _effectiveActiveBorderColor = widget.activeBorderColor ?? - context.moonTheme?.authCodeTheme.colors.activeBorderColor ?? - MoonColors.light.beerus; - - _effectiveInactiveBorderColor = widget.inactiveBorderColor ?? - context.moonTheme?.authCodeTheme.colors.inactiveBorderColor ?? - MoonColors.light.beerus; - - _effectiveErrorBorderColor = widget.errorBorderColor ?? - context.moonTheme?.authCodeTheme.colors.errorBorderColor ?? - MoonColors.light.chichi; - - _effectiveSelectedFillColor = widget.selectedFillColor ?? - context.moonTheme?.authCodeTheme.colors.selectedFillColor ?? - MoonColors.light.goku; - - _effectiveActiveFillColor = widget.activeFillColor ?? - context.moonTheme?.authCodeTheme.colors.activeFillColor ?? - MoonColors.light.goku; - - _effectiveInactiveFillColor = widget.inactiveFillColor ?? - context.moonTheme?.authCodeTheme.colors.inactiveFillColor ?? - MoonColors.light.goku; - - _effectiveTextStyle = - context.moonTheme?.authCodeTheme.properties.textStyle ?? - MoonTypography.typography.body.text24; - - _effectiveErrorTextStyle = - context.moonTheme?.authCodeTheme.properties.errorTextStyle ?? - MoonTypography.typography.body.text12; - - _effectiveTextColor = context.moonTheme?.authCodeTheme.colors.textColor ?? - MoonColors.light.textPrimary; - - _effectiveCursorColor = widget.authFieldCursorColor ?? - context.moonTheme?.authCodeTheme.colors.textColor ?? - MoonColors.light.textPrimary; - - _animationDuration ??= widget.animationDuration ?? - context.moonTheme?.authCodeTheme.properties.animationDuration ?? - MoonTransitions.transitions.defaultTransitionDuration; - - _animationCurve ??= widget.animationCurve ?? - context.moonTheme?.authCodeTheme.properties.animationCurve ?? - MoonTransitions.transitions.defaultTransitionCurve; - - _peekDuration ??= widget.peekDuration ?? - context.moonTheme?.authCodeTheme.properties.peekDuration ?? - MoonTransitions.transitions.defaultTransitionDuration; - - final double effectiveDisabledOpacityValue = widget.disabledOpacityValue ?? - context.moonOpacities?.disabled ?? - MoonOpacities.opacities.disabled; - - final Duration effectiveErrorAnimationDuration = widget - .errorAnimationDuration ?? - context.moonTheme?.authCodeTheme.properties.errorAnimationDuration ?? - MoonTransitions.transitions.defaultTransitionDuration; - - final Curve effectiveErrorAnimationCurve = widget.errorAnimationCurve ?? - context.moonTheme?.authCodeTheme.properties.errorAnimationCurve ?? - MoonTransitions.transitions.defaultTransitionCurve; - - _errorAnimationController ??= AnimationController( - duration: effectiveErrorAnimationDuration, - vsync: this, + final Style errorTextDefaultStyle = Style( + $with.defaultTextStyle.style.as(resolvedErrorTextStyle), + $with.iconTheme.data.color(effectiveErrorColor), ); - _errorOffsetAnimation ??= Tween( - begin: Offset.zero, - end: const Offset(.01, 0.0), - ).animate( - CurvedAnimation( - parent: _errorAnimationController!, - curve: effectiveErrorAnimationCurve, - ), + final Style helperTextDefaultStyle = Style( + $with.defaultTextStyle.style.as(resolvedHelperTextStyle), + $with.iconTheme.data.color(effectiveTextColor), ); - return Semantics( - label: widget.semanticLabel, - child: RepaintBoundary( - child: AnimatedOpacity( - duration: _animationDuration!, - curve: _animationCurve!, - opacity: widget.enabled ? 1 : effectiveDisabledOpacityValue, - child: Column( - children: [ - SlideTransition( - position: _errorOffsetAnimation!, - child: Stack( - children: [ - AbsorbPointer( - child: AutofillGroup( - child: _getTextFormField(), - ), - ), - Positioned( - top: 0, - left: 0, - right: 0, - child: GestureDetector( - onTap: () => _onFocus(), - child: Row( - mainAxisAlignment: widget.mainAxisAlignment, - children: _generateAuthInputFields(), - ), - ), - ), - ], - ), - ), - if (_isInErrorMode) - DefaultTextStyle( - style: _resolvedErrorTextStyle, - child: IconTheme( - data: IconThemeData( - color: _resolvedErrorTextStyle.color, - ), - child: widget.errorBuilder( - context, - _validateInput() ?? widget.errorText, - ), - ), - ), - ], - ), - ), + return MoonRawAuthCode( + enabled: enabled, + authInputFieldCount: authInputFieldCount, + semanticLabel: semanticLabel, + useHapticFeedback: useHapticFeedback, + showCursor: showCursor, + cursorColor: cursorColor, + cursorErrorColor: cursorErrorColor, + focusNode: focusNode, + autoFocus: autoFocus, + autoUnfocus: autoUnfocus, + keyboardType: keyboardType, + autoDismissKeyboard: autoDismissKeyboard, + inputFieldStyle: inputFieldStyle, + errorText: errorText, + errorAnimationType: errorAnimationType, + errorAnimationCurve: effectiveErrorAnimationCurve, + errorAnimationDuration: effectiveErrorAnimationDuration, + textInputAction: textInputAction, + inputFormatters: inputFormatters, + obscureText: obscureText, + obscuringCharacter: obscuringCharacter, + obscuringWidget: obscuringWidget, + peekDuration: effectivePeekDuration, + peekWhenObscuring: peekWhenObscuring, + textController: textController, + validator: validator, + onChanged: onChanged, + onCompleted: onCompleted, + onEditingComplete: onEditingComplete, + onSubmitted: onSubmitted, + hint: hint, + errorBuilder: (BuildContext _, String? errorText) => SpecBuilder( + style: errorTextDefaultStyle, + builder: (BuildContext context) => errorBuilder(context, errorText), + ), + helperText: SpecBuilder( + style: helperTextDefaultStyle, + builder: (BuildContext _) => helperText ?? const SizedBox.shrink(), ), ); } } - -class _CursorPainter extends CustomPainter { - final Color cursorColor; - - _CursorPainter({required this.cursorColor}); - - @override - void paint(Canvas canvas, Size size) { - const Offset p1 = Offset.zero; - final Offset p2 = Offset(0, size.height); - final Paint paint = Paint() - ..color = cursorColor - ..strokeWidth = 2; - - canvas.drawLine(p1, p2, paint); - } - - @override - bool shouldRepaint(CustomPainter old) => false; -}