Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use BLOC pattern for an authentication form

Tags:

flutter

I am trying to use BLOC pattern on a basic authentication form which contains both login and signup, where the only difference between login and signup is that signup has an additional Confirm Password field which also contributes to whether Signup button should be enabled.

I have two questions: 1. This is a problem. Currently, if I enter some login passing the Login validation, then switch to Signup form, the Signup button is enabled which is wrong because Confirm Password is still empty. How to fix this? 2. I feel like there is a better way than what I have done to achieve the Confirm Password validation and Signup button validation. I initially tried to create a validator for Confirm Password but it shall take both password and confirm password as an input but couldn't get it working as StreamTransformer only take one input parameter. What's the better way of doing this?

import 'package:flutter/material.dart';
import 'dart:async';
import 'package:rxdart/rxdart.dart';


void main() => runApp(AuthProvider(child: MaterialApp(home: Auth())));

enum AuthMode { Signup, Login }

class Auth extends StatefulWidget {
  @override
  _AuthState createState() => _AuthState();
}

class _AuthState extends State<Auth> {
  AuthMode authMode = AuthMode.Login;
  bool get _isLoginMode => authMode == AuthMode.Login;
  TextEditingController confirmPasswordCtrl = TextEditingController();

  @override
  Widget build(BuildContext context) {
    final bloc = AuthProvider.of(context);
    return Scaffold(
      body: Container(
        margin: EdgeInsets.all(20.0),
        child: Column(
          children: <Widget>[
            emailField(bloc),
            passwordField(bloc),
            confirmPasswordField(bloc),
            Container(
              margin: EdgeInsets.only(top: 40.0),
            ),
            FlatButton(
              child: Text('Switch to ${_isLoginMode ? 'Signup' : 'Login'}'),
              onPressed: swithAuthMode,
            ),
            loginOrSignupButton(bloc),
          ],
        ),
      ),
    );
  }

  void swithAuthMode() {
    setState(() {
      authMode = authMode == AuthMode.Login ? AuthMode.Signup : AuthMode.Login;
    });
  }

  Widget confirmPasswordField(AuthBloc bloc) {
    return _isLoginMode
        ? Container()
        : StreamBuilder(
            stream: bloc.passwordConfirmed,
            builder: (context, snapshot) {
              return TextField(
                obscureText: true,
                onChanged: bloc.changeConfirmPassword,
                keyboardType: TextInputType.text,
                decoration: InputDecoration(
                  labelText: 'Confirm Password',
                  errorText: snapshot.hasData && !snapshot.data ? 'password mismatch' : null,
                ),
              );
            },
          );
  }

  Widget emailField(AuthBloc bloc) {
    return StreamBuilder(
      stream: bloc.email,
      builder: (context, snapshot) {
        return TextField(
          keyboardType: TextInputType.emailAddress,
          onChanged: bloc.changeEmail,
          decoration: InputDecoration(
            hintText: 'your email',
            labelText: 'Email',
            errorText: snapshot.error,
          ),
        );
      },
    );
  }

  Widget loginOrSignupButton(AuthBloc bloc) {
    return StreamBuilder(
      stream: _isLoginMode ? bloc.submitValid : bloc.signupValid,
      builder: (context, snapshot) {
        print('hasData: ${snapshot.hasData}, data: ${snapshot.data}');
        return RaisedButton(
          onPressed: // The problem is, after entering some login details then switching from login to signup, the Signup button is enabled.
              !snapshot.hasData || !snapshot.data ? null : () => onSubmitPressed(bloc, context),
          color: Colors.blue,
          child: Text('${_isLoginMode ? 'Log in' : 'Sign up'}'),
        );
      },
    );
  }

  void onSubmitPressed(AuthBloc bloc, BuildContext context) async {
    var response = await bloc.submit(_isLoginMode);
    if (response.success) {
      Navigator.pushReplacementNamed(context, '/home');
    } else {
      showDialog(
          context: context,
          builder: (context) {
            return AlertDialog(
              title: Text('Error'),
              content: Text(response.message),
              actions: <Widget>[
                FlatButton(
                  child: Text('Ok'),
                  onPressed: () {
                    Navigator.of(context).pop();
                  },
                ),
              ],
            );
          });
    }
  }

  Widget passwordField(AuthBloc bloc) {
    return StreamBuilder(
      stream: bloc.password,
      builder: (_, snapshot) {
        return TextField(
          obscureText: true,
          onChanged: bloc.changePassword,
          decoration: InputDecoration(
            labelText: 'Password',
            errorText: snapshot.error,
            hintText: 'at least 6 characters',
          ),
        );
      },
    );
  }
}

class AuthProvider extends InheritedWidget {
  final bloc;

  AuthProvider({Key key, Widget child}) :
    bloc = AuthBloc(), super(key:key, child: child);

  @override
  bool updateShouldNotify(InheritedWidget oldWidget) => true;

  static AuthBloc of(BuildContext context) => (context.inheritFromWidgetOfExactType(AuthProvider) as AuthProvider).bloc;

}

 class Repository {
   // this will call whatever backend to authenticate users.
  Future<AuthResult> signupUser(String email, String password) => null;
  Future<AuthResult> loginUser(String email, String password) => null;
}


class AuthBloc extends Object with AuthValidator {
  final _emailController = BehaviorSubject<String>();
  final _passwordController = BehaviorSubject<String>();
  final _confirmPasswordController = BehaviorSubject<String>();
  final _signupController = PublishSubject<Map<String, dynamic>>();
  final Repository _repository = Repository();

  Stream<String> get email => _emailController.stream.transform(validateEmail);

  Stream<String> get password =>
      _passwordController.stream.transform(validatePassword);

  Stream<bool> get submitValid =>
      Observable.combineLatest2(email, password, (e, p) => true);

  // Is there a better way of doing passwordConfirmed and signupValid?
  Stream<bool> get passwordConfirmed =>
      Observable.combineLatest2(password, _confirmPasswordController.stream, (p, cp) => p == cp);

  Stream<bool> get signupValid =>
      Observable.combineLatest2(submitValid, passwordConfirmed, (s, p) => s && p);


  // sink
  Function(String) get changeEmail => _emailController.sink.add;
  Function(String) get changePassword => _passwordController.sink.add;
  Function(String) get changeConfirmPassword =>
      _confirmPasswordController.sink.add;

  Future<AuthResult> submit(bool isLogin) async {
    final validEmail = _emailController.value;
    final validPassword = _passwordController.value;
    if (!isLogin)
      return await _repository.signupUser(validEmail, validPassword);

    return await _repository.loginUser(validEmail, validPassword);
  }

  void dispose() {
    _emailController.close();
    _passwordController.close();
    _signupController.close();
    _confirmPasswordController.close();
  }
}

class AuthResult {
  bool success;
  String message;
  AuthResult(this.success, this.message);
}

// demo validator
class AuthValidator {
  final validateEmail = StreamTransformer<String, String>.fromHandlers(
    handleData: (email, sink) {
      if (email.contains('@')) sink.add(email);
      else sink.addError('Email is not valid');
    }
  );

  final validatePassword = StreamTransformer<String, String>.fromHandlers(
    handleData: (password, sink) {
      if (password.length >= 6) sink.add(password);
      else sink.addError('Password must be at least 6 characters');
    }
  );
}
like image 509
stt106 Avatar asked Jun 04 '26 02:06

stt106


2 Answers

In your case, a better way to do passwordConfirmed would be:

Stream<String> get passwordConfirmed => _confirmPasswordController.stream
    .transform(validatePassword).doOnData((String confirmPassword){
        if(0 != _passwordController.value.compareTo(confirmPassword)){
            _confirmPasswordController.addError("Passwords do not match");
        }
});

As suggested by boeledi here.

like image 77
pat64j Avatar answered Jun 07 '26 23:06

pat64j


After trying to replicate the behavior I could confirm that signupValid stream has a true value if submitValid has a true value, seems like the computation for signupValid is never being performed.

One work around would be to clear the text fields and adding an empty string to the streams on changing the login mode from login to sign up and vice versa.

like image 27
ssiddh Avatar answered Jun 07 '26 22:06

ssiddh