dragonfly02
dragonfly02

Reputation: 3669

Use BLOC pattern for an authentication form

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');
    }
  );
}

Upvotes: 3

Views: 6000

Answers (3)

Tanvirul Islam
Tanvirul Islam

Reputation: 106

After a few days of analyzing I got a more effective way to handle auth using bloc. You can follow.

1st Update Login Info Stream under Sign In Bloc

 // Handle User Name Info From Login UI
 final _userNameController = BehaviorSubject<String>();
  Stream<String> get userNameStream => _userNameController.stream;

  void updateUserName(String userName) {
    if (userName.isEmpty) {
      _userNameController.addError("Please Enter Username");
      return;
    }
    _userNameController.sink.add(userName);
    return;
  }

  // handle User Password From Login UI
  final _passwordController = BehaviorSubject<String>();
  Stream<String> get passwordStrem => _passwordController.stream;

  void updatePassword(String password) async {
    if (password.isEmpty) {
      _passwordController.addError("Please Enter Password");
      return;
    } else if (password.length < 6) {
      _passwordController.addError("Password Must be At least 6");
      return;
    }
    _passwordController.sink.add(password);
    return;
  }



 // User Login button validation bloc code starts here
  Stream<bool> get isValidButton => Rx.combineLatest2(
      passwordStrem,
      userNameStream,
      (a, b,) => true );

2d Make sure properly injected your Login Bloc in Login Page. Now Call Bloc To check user Login, Auth

                    StreamBuilder<String>(
                      stream: context.read<SignInBloc>().userNameStream,
                      builder: (context, snapshot) {
                        return TextFormField(
                          controller: usernameController,
                          decoration: InputDecoration(
                          errorText: snapshot.error as String?, 
                          ),
                          onChanged: (value) {
                            context.read<SignInBloc>() .updateUserName(value);
                          },
                        );
                      }),
              
                  StreamBuilder<String>(
                      stream: context.read<SignInBloc>().passwordStrem,
                      builder: (context, snapshot) {
                        return TextFormField(
                          controller: passwordController,
                          cursorColor:
                              const Color(0xff538234),
                          obscureText: showPass,
                          decoration: InputDecoration(
                            errorText:snapshot.error as String?,
                          ),
                          onChanged: (value) {
                            context.read<SignInBloc>().updatePassword(value);
                          },
                        );
                      }),

3rd is Login Button Validation

                                StreamBuilder<bool>(
                                  stream: context.read<SignInBloc().isValidButton,
                                  builder: (context, snapshot) {
                                    bool isValid =snapshot.data ?? false;
                                    return ButtonPrimary(
                                      name: S.of(context).login,
                                      onPressed: !isValid
                                          ? null
                                          : () async {
                                   // CALL YOUR LOGIN API WHEN BUTTON IS VALID

                                        }
                                    );
                                  }),

Upvotes: 0

pat64j
pat64j

Reputation: 1121

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.

Upvotes: 2

ssiddh
ssiddh

Reputation: 520

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.

Upvotes: 1

Related Questions