Snippets for logging in. Phone verification works with sign up too
Dependencies
Add the following packages in the pubspec.yaml first.
- cloud_firestore: ^version
- firebase_auth: ^version
- flutter_bloc: ^version
- equatable: ^version
- pin_entry_text_field: ^version (optional. Used for getting the OTP code from the user)
- 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));
})
],
);
}
}