Sign in with phone number authentication & Firebase

Snippets for logging in. Phone verification works with sign up too

Dependencies

Add the following packages in the pubspec.yaml first.

  1. cloud_firestore: ^version
  2. firebase_auth: ^version
  3. flutter_bloc: ^version
  4. equatable: ^version
  5. pin_entry_text_field: ^version (optional. Used for getting the OTP code from the user)
  6. country_pickers: ^version (optional. Used for showing a country phone code picker in front of the phone edit text)

and run the pub get command from the terminal.

login_bloc.dart

class LoginBloc extends Bloc<LoginEvent, LoginState> {
  AuthenticationRepository _authenticationRepository =
      AuthenticationRepository();

  StreamSubscription subscription;

  String verificationId = "";

  LoginBloc() : super(InitialLoginState());

  @override
  LoginState get initialState => InitialLoginState();

  @override
  Stream<LoginState> mapEventToState(
    LoginEvent event,
  ) async* {
    if (event is SendOtpEvent) {
      yield LoadingState();
      subscription = sendOtp(event.phone).listen((event) {
        add(event);
      });
    } else if (event is OtpSendEvent) {
      yield OtpSentState();
    } else if (event is LoginCompleteEvent) {
      yield LoginCompleteState();
    } else if (event is LoginExceptionEvent) {
      yield ExceptionState(message: event.message);
    } else if (event is VerifyOtpEvent) {
      yield LoadingState();
      try {
        var result = await _authenticationRepository.verifyAndLogin(
            verificationId, event.otp);
        if (result.user != null) {
          yield LoginCompleteState();
        } else {
          yield OtpExceptionState(message: "Invalid otp!");
        }
      } catch (e) {
        yield OtpExceptionState(message: "Invalid otp!");
        print(e);
      }
    }
  }

  @override
  void onError(Object error, StackTrace stacktrace) {
    super.onError(error, stacktrace);
    ....
    print(stacktrace);
  }

  Future<void> close() async {
    print("Bloc closed");
    super.close();
  }

  Stream<LoginEvent> sendOtp(String phoNo) async* {
    StreamController<LoginEvent> eventStream = StreamController();
    final PhoneVerificationCompleted = (AuthCredential authCredential) {
      _authenticationRepository.user;
      _authenticationRepository.user.single.catchError((onError) {
        print(onError);
      }).then((_) {
        eventStream.add(LoginCompleteEvent());
        eventStream.close();
      });
    };
    final PhoneVerificationFailed = (FirebaseAuthException authException) {
      print(authException.message);
      eventStream.add(LoginExceptionEvent(authException.message));
      eventStream.close();
    };
    final PhoneCodeSent = (String verId, [int forceResent]) {
      this.verificationId = verId;
      eventStream.add(OtpSendEvent());
    };
    final PhoneCodeAutoRetrievalTimeout = (String verid) {
      this.verificationId = verid;
      eventStream.close();
    };

    await _authenticationRepository.sendOtp(
        phoNo,
        Duration(minutes: 2),
        PhoneVerificationFailed,
        PhoneVerificationCompleted,
        PhoneCodeSent,
        PhoneCodeAutoRetrievalTimeout);

    yield* eventStream.stream;
  }
}

login_event.dart

class LoginEvent extends Equatable {
  @override
  List<Object> get props => [];
}

class SendOtpEvent extends LoginEvent {
  String phone;

  SendOtpEvent({this.phone});
}

class AppStartEvent extends LoginEvent {}

class VerifyOtpEvent extends LoginEvent {
  String otp;

  VerifyOtpEvent({this.otp});
}

class LogoutEvent extends LoginEvent {}

class OtpSendEvent extends LoginEvent {}

class LoginCompleteEvent extends LoginEvent {}

class LoginExceptionEvent extends LoginEvent {
  String message;

  LoginExceptionEvent(this.message);
}

login_state.dart

@immutable
abstract class LoginState extends Equatable {}

class InitialLoginState extends LoginState {
  @override
  // TODO: implement props
  List<Object> get props => [];
}

class OtpSentState extends LoginState {
  @override
  List<Object> get props => [];
}

class LoadingState extends LoginState {
  @override
  List<Object> get props => [];
}

class OtpVerifiedState extends LoginState {
  @override
  List<Object> get props => [];
}

class LoginCompleteState extends LoginState {
  @override
  List<Object> get props => [];
}

class ExceptionState extends LoginState {
  String message;

  ExceptionState({this.message});

  @override
  // TODO: implement props
  List<Object> get props => [message];
}

class OtpExceptionState extends LoginState {
  String message;

  OtpExceptionState({this.message});

  @override
  // TODO: implement props
  List<Object> get props => [message];
}

Login widget

class LoginScreen extends StatelessWidget {
  LoginScreen({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocProvider<LoginBloc>(
      create: (context) => LoginBloc(),
      child: Scaffold(
        body: LoginForm(),
      ),
    );
  }
}

class LoginForm extends StatefulWidget {
  @override
  _LoginFormState createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  @override
  Widget build(BuildContext context) {
    return BlocListener<LoginBloc, LoginState>(
      cubit: BlocProvider.of<LoginBloc>(context),
      listener: (context, state) {
        print("login state:: $state");
        if (state is ExceptionState || state is OtpExceptionState) {
          String message;
          if (state is ExceptionState) {
            message = state.message;
          } else if (state is OtpExceptionState) {
            message = state.message;
          }
          Scaffold.of(context)
            ..hideCurrentSnackBar()
            ..showSnackBar(
              SnackBar(content: Text(message)),
            );
        }
      },
      child: BlocBuilder<LoginBloc, LoginState>(
        builder: (context, state) {
          return Scaffold(
            body: SingleChildScrollView(
              child: getViewAsPerState(state),
            ),
          );
        },
      ),
    );
  }

  getViewAsPerState(LoginState state) {
    if (state is OtpSentState || state is OtpExceptionState) {
      return OtpInput();
    } else if (state is LoadingState) {
      return LoadingIndicator();
    } else if (state is LoginCompleteState) {
      // navigate away 
      Container();
    } else {
      return PhoneInput();
    }
  }
}

class LoadingIndicator extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Center(
        child: CircularProgressIndicator(),
      );
}

class PhoneInput extends StatelessWidget {
  final _formKey = GlobalKey<FormState>();
  final _phoneTextController = TextEditingController();
  var selectedCountryCode =
      CountryPickerUtils.getCountryByIsoCode('NL').phoneCode;

  _buildCountryPickerDropdown(BuildContext context) {
    return CountryPickerDropdown(
      onTap: () => FocusScope.of(context).requestFocus(FocusNode()),
      onValuePicked: (Country country) {
        selectedCountryCode = country.phoneCode;
      },
      itemBuilder: (Country country) {
        return Row(
          children: <Widget>[
            SizedBox(width: Dimens.padding),
            CountryPickerUtils.getDefaultFlagImage(country),
            SizedBox(width: Dimens.padding),
            Container(child: Text(country.phoneCode)),
          ],
        );
      },
      itemHeight: null,
      isExpanded: true,
      initialValue: 'NL',
      priorityList: [
        CountryPickerUtils.getCountryByIsoCode('NL'),
        CountryPickerUtils.getCountryByIsoCode('BE'),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Expanded(
              flex: 1,
              child: new Padding(
                padding: const EdgeInsets.all(Dimens.padding),
                child: _buildCountryPickerDropdown(context),
              ),
            ),
            Expanded(
              flex: 2,
              child: new Padding(
                padding: const EdgeInsets.only(
                    right: Dimens.paddingBig, left: Dimens.padding),
                child: Form(
                  key: _formKey,
                  child: TextFormField(
                    decoration: InputDecoration(
                        border: InputBorder.none,
                        focusedBorder: InputBorder.none,
                        enabledBorder: InputBorder.none,
                        errorBorder: InputBorder.none,
                        disabledBorder: InputBorder.none,
                        errorMaxLines: 2,
                        hintText: Strings.hintPhone),
                    validator: (value) {
                      return validateMobile(value);
                    },
                    keyboardType: TextInputType.phone,
                    controller: _phoneTextController,
                  ),
                ),
              ),
            ),
          ],
        ),
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: RaisedButton(
            onPressed: () {
              if (_formKey.currentState.validate()) {
                BlocProvider.of<LoginBloc>(context).add(SendOtpEvent(
                    phone: "+${selectedCountryCode +
                        _phoneTextController.value.text}"));
              }
            },
            color: Colors.orange,
            child: Text(
              Strings.btContinue,
              style: TextStyle(color: Colors.white),
            ),
          ),
        )
      ],
    );
  }

  String validateMobile(String value) {
    RegExp _phoneRegExp = RegExp(
      r'(^[0-9]{8,12}$)',
    );
    return _phoneRegExp.hasMatch(value) ? null : Strings.errorPhoneInvalid;
  }
}

class OtpInput extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        PinEntryTextField(
            fields: 6,
            onSubmit: (String pin) {
              BlocProvider.of<LoginBloc>(context)
                  .add(VerifyOtpEvent(otp: pin));
            })
      ],
    );
  }
}