Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: [MDS-920] Fix AuthCode validation #328

Merged
merged 1 commit into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 8 additions & 32 deletions example/assets/code_snippets/auth_code.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,22 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:moon_design/moon_design.dart';

class AuthCode extends StatefulWidget {
class AuthCode extends StatelessWidget {
const AuthCode({super.key});

@override
State<AuthCode> createState() => _AuthCodeState();
}

class _AuthCodeState extends State<AuthCode> {
late StreamController<ErrorAnimationType> _errorStreamController;

@override
void initState() {
super.initState();

_errorStreamController = StreamController<ErrorAnimationType>();
}

@override
void dispose() {
_errorStreamController.close();

super.dispose();
}

@override
Widget build(BuildContext context) {
return SizedBox(
height: 95, // To avoid widget jumping with error text, use a fixed-height wrapper.
child: MoonAuthCode(
errorStreamController: _errorStreamController,
onCompleted: (String pin) {
if (pin != '123456') {
_errorStreamController.add(ErrorAnimationType.shake);
}
validator: (String? pin) {
// Matches all numbers
final RegExp regex = RegExp(r'^\d+$');

return pin != null && pin.length == 4 && !regex.hasMatch(pin)
? 'The input must only contain numbers'
: null;
},
validator: (String? pin) => pin?.length == 6 && pin != '123456'
? 'Invalid authentication code. Please try again.'
: null,
errorBuilder: (BuildContext context, String? errorText) {
return Align(
child: Padding(
Expand Down
41 changes: 8 additions & 33 deletions example/lib/src/storybook/stories/auth_code.dart
Original file line number Diff line number Diff line change
@@ -1,37 +1,14 @@
import 'dart:async';

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_design/moon_design.dart';
import 'package:storybook_flutter/storybook_flutter.dart';

class AuthCodeStory extends StatefulWidget {
class AuthCodeStory extends StatelessWidget {
static const path = '/auth_code';

const AuthCodeStory({super.key});

@override
State<AuthCodeStory> createState() => _AuthCodeStoryState();
}

class _AuthCodeStoryState extends State<AuthCodeStory> {
late StreamController<ErrorAnimationType> _errorStreamController;

@override
void initState() {
super.initState();

_errorStreamController = StreamController<ErrorAnimationType>();
}

@override
void dispose() {
_errorStreamController.close();

super.dispose();
}

@override
Widget build(BuildContext context) {
final mainAxisAlignmentKnob = context.knobs.nullable.options(
Expand Down Expand Up @@ -246,8 +223,8 @@ class _AuthCodeStoryState extends State<AuthCodeStory> {
enableInputFill: true,
authInputFieldCount: 4,
mainAxisAlignment: mainAxisAlignmentKnob ?? MainAxisAlignment.center,
errorAnimationType: errorAnimationKnob ? ErrorAnimationType.shake : ErrorAnimationType.noAnimation,
borderRadius: borderRadius,
errorStreamController: _errorStreamController,
textStyle: TextStyle(color: textColor),
authFieldCursorColor: cursorColor,
selectedFillColor: selectedFillColor,
Expand All @@ -260,15 +237,13 @@ class _AuthCodeStoryState extends State<AuthCodeStory> {
authFieldShape: shapeKnob,
obscureText: obscuringKnob,
peekWhenObscuring: peekWhenObscuringKnob,
onCompleted: (String pin) {
if (pin != '9921') {
_errorStreamController.add(
errorAnimationKnob ? ErrorAnimationType.shake : ErrorAnimationType.noAnimation,
);
}
},
validator: (String? pin) {
return pin?.length == 4 && pin != '9921' ? 'Invalid authentication code. Please try again.' : null;
// Matches all numbers
final RegExp regex = RegExp(r'^\d+$');

return pin != null && pin.length == 4 && !regex.hasMatch(pin)
? 'The input must only contain numbers'
: null;
},
errorBuilder: (BuildContext context, String? errorText) {
return Align(
Expand Down
94 changes: 54 additions & 40 deletions lib/src/widgets/authcode/authcode.dart
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,12 @@ class MoonAuthCode extends StatefulWidget {
/// AuthCode error animation curve.
final Curve? errorAnimationCurve;

/// Animation type for validation error. Default is [ErrorAnimationType.noAnimation].
final ErrorAnimationType errorAnimationType;

/// {@macro flutter.widgets.Focus.focusNode}.
final FocusNode? focusNode;

/// Validator for the [TextFormField].
final FormFieldValidator<String> validator;

/// Count of auth input fields.
final int authInputFieldCount;

Expand All @@ -143,8 +143,10 @@ class MoonAuthCode extends StatefulWidget {
/// Controls how auth input fields are aligned on the main axis.
final MainAxisAlignment mainAxisAlignment;

/// Builder for the error widget.
final MoonAuthCodeErrorBuilder errorBuilder;
/// Error text can be used to force Authcode to be in error state (useful for async errors).
///
/// Note that validator errors have precedence over passed in [errorText].
final String? errorText;

/// Displays a hint or a placeholder in the input field if it's value is empty.
final String? hintCharacter;
Expand All @@ -157,12 +159,6 @@ class MoonAuthCode extends StatefulWidget {
/// Semantic label for MoonAuthCode.
final String? semanticLabel;

/// MoonAuthCode error stream controller.
final StreamController<ErrorAnimationType>? errorStreamController;

/// [TextEditingController] for an editable text field.
final TextEditingController? textController;

/// An action user has requested the text input control to perform.
final TextInputAction textInputAction;

Expand All @@ -178,6 +174,12 @@ class MoonAuthCode extends StatefulWidget {
/// MoonAuthCode error text style.
final TextStyle? errorTextStyle;

/// [TextEditingController] for an editable text field.
final TextEditingController? textController;

/// Validator for the [TextFormField].
final FormFieldValidator<String> validator;

/// Returns current auth input text.
final ValueChanged<String>? onChanged;

Expand All @@ -194,6 +196,9 @@ class MoonAuthCode extends StatefulWidget {
/// Set this to empty function if keyboard should not close automatically on done/next press.
final VoidCallback? onEditingComplete;

/// Builder for the error widget.
final MoonAuthCodeErrorBuilder errorBuilder;

/// Widget used to obscure text.
///
/// Overrides the [obscuringCharacter].
Expand Down Expand Up @@ -221,38 +226,39 @@ class MoonAuthCode extends StatefulWidget {
this.selectedFillColor,
this.activeFillColor,
this.inactiveFillColor,
this.height,
this.width,
this.borderWidth,
this.disabledOpacityValue,
this.gap,
this.errorAnimationCurve,
this.animationCurve,
this.errorAnimationDuration,
this.height,
this.width,
this.animationDuration,
this.errorAnimationDuration,
this.peekDuration,
this.animationCurve,
this.errorAnimationCurve,
this.errorAnimationType = ErrorAnimationType.noAnimation,
this.focusNode,
required this.validator,
this.authInputFieldCount = 6,
this.boxShadows,
this.activeBoxShadows,
this.inActiveBoxShadows,
this.mainAxisAlignment = MainAxisAlignment.center,
required this.errorBuilder,
this.hintCharacter,
this.obscuringCharacter = '•',
this.semanticLabel,
this.errorStreamController,
this.textController,
this.textInputAction = TextInputAction.done,
this.keyboardType = TextInputType.visiblePassword,
this.errorText,
this.hintStyle,
this.textStyle,
this.errorTextStyle,
this.textController,
required this.validator,
this.onChanged,
this.onCompleted,
this.onEditingComplete,
this.onSubmitted,
this.onEditingComplete,
required this.errorBuilder,
this.obscuringWidget,
}) : assert(authInputFieldCount > 0),
assert(height == null || height > 0),
Expand Down Expand Up @@ -287,9 +293,9 @@ class _MoonAuthCodeState extends State<MoonAuthCode> with TickerProviderStateMix
late AnimationController _cursorController;
late Animation<double> _cursorAnimation;

StreamSubscription<ErrorAnimationType>? _errorAnimationSubscription;
AnimationController? _errorAnimationController;
Animation<Offset>? _errorOffsetAnimation;

Duration? _peekDuration;
Duration? _animationDuration;
Curve? _animationCurve;
Expand All @@ -315,9 +321,9 @@ class _MoonAuthCodeState extends State<MoonAuthCode> with TickerProviderStateMix
void _initializeFields() {
_initializeFocusNode();
_initializeInputList();
_initializeErrorAnimationListener();
_initializeTextEditingController();
_initializeAuthFieldCursor();
_initializeErrorAnimationListener();
}

void _initializeFocusNode() {
Expand All @@ -333,11 +339,18 @@ class _MoonAuthCodeState extends State<MoonAuthCode> with TickerProviderStateMix
_textEditingController = widget.textController ?? TextEditingController();

_textEditingController.addListener(() {
if (_isInErrorMode) {
_setState(() => _isInErrorMode = false);
}
// We use custom error builder and thus need to validate input manually to show validation error.
// _validateInput() returns error String in case of error, otherwise null.
if (_validateInput() != null) {
if (widget.errorAnimationType == ErrorAnimationType.shake) {
_errorAnimationController!.forward();

if (widget.useHapticFeedback) HapticFeedback.lightImpact();
if (widget.useHapticFeedback) HapticFeedback.lightImpact();
}
if (!_isInErrorMode) _setState(() => _isInErrorMode = true);
} else {
if (_isInErrorMode && widget.errorText == null) _setState(() => _isInErrorMode = false);
}

_debounceBlink();

Expand Down Expand Up @@ -381,17 +394,6 @@ class _MoonAuthCodeState extends State<MoonAuthCode> with TickerProviderStateMix
_errorAnimationController!.addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) _errorAnimationController!.reverse();
});

if (widget.errorStreamController != null) {
_errorAnimationSubscription = widget.errorStreamController!.stream.listen((ErrorAnimationType errorAnimation) {
if (errorAnimation == ErrorAnimationType.shake) {
_errorAnimationController!.forward();
}
_setState(() => _isInErrorMode = true);

if (widget.useHapticFeedback) HapticFeedback.vibrate();
});
}
});
}

Expand Down Expand Up @@ -505,16 +507,28 @@ class _MoonAuthCodeState extends State<MoonAuthCode> with TickerProviderStateMix
void initState() {
super.initState();

_isInErrorMode = widget.errorText != null;

_initializeFields();
}

@override
void didUpdateWidget(MoonAuthCode oldWidget) {
super.didUpdateWidget(oldWidget);

if (oldWidget.errorText != widget.errorText) {
_setState(() {
_isInErrorMode = widget.errorText != null || _validateInput() != null;
});
}
}

@override
void dispose() {
_textEditingController.dispose();
_errorAnimationController!.dispose();
_cursorController.dispose();
_focusNode.dispose();
_errorAnimationSubscription?.cancel();

super.dispose();
}
Expand Down Expand Up @@ -795,7 +809,7 @@ class _MoonAuthCodeState extends State<MoonAuthCode> with TickerProviderStateMix
data: IconThemeData(
color: _resolvedErrorTextStyle.color,
),
child: widget.errorBuilder(context, _validateInput()),
child: widget.errorBuilder(context, _validateInput() ?? widget.errorText),
),
),
],
Expand Down