Я использую FirebaseAuth для проверки телефона, адаптируя это руководство https://medium.com/@tapanrgohil/firebase-phone-authentication-in-flutter-with-bloc-pattern-4ddc2d43d76c, так как я не хочу входить в систему, а просто привяжу новую аутентификацию PhoneAuthentication к существующей user, и я использую AuthenticationBloc
вместо Losing Bloc в руководстве.
Я начинаю процесс проверки телефона в PaymentScreen
и пытался указать AuthenticationBloc
непосредственно в PaymentScreen
MultiBlocProvider
, думая создать новый AuthenticationBloc
, но ошибка та же.
В AuthenticationBloc
в основном внутренняя StreamController
заботится обо всех событиях проверки телефона. Входящий States
в PaymentScreen
BlocListener
просто реагирует, выскакивая и показывая диалоги, как в случае AutoRetrieveCodeTimeout
, показывающего диалог ручной вставки OTP, ошибки, неправильный OTP и так далее. Чтобы выяснить, что вызывает плохое состояние, я сначала закомментировал все всплывающие контекстные окна, чтобы убедиться, что это не так, а затем я закомментировал все .close()
в потоке.
Это отпечатки с консоли:
I/flutter ( 7710): VerifyPhoneNumberEvent received
I/flutter ( 7710): _mapVerifyPhoneNumberToState started
I/BiChannelGoogleApi( 7710): [FirebaseAuth: ] getGoogleApiForMethod() returned Gms: com.google.firebase.auth.api.internal.zzaq@7f6fccb
I/flutter ( 7710): _mapVerifyPhoneNumberToState PhoneCodeSent
I/flutter ( 7710): PhoneCodeSentEvent received
I/flutter ( 7710): _mapVerifyPhoneNumberToState PhoneCodeAutoRetrievalTimeout
I/flutter ( 7710): Bloc error is Bad state: Cannot add new events after calling close
I/flutter ( 7710): Bloc error is Bad state: Cannot add new events after calling close
I/flutter ( 7710): Bloc error is Bad state: Cannot add new events after calling close
I/flutter ( 7710): Bloc error is Bad state: Cannot add new events after calling close
I/flutter ( 7710): Bloc error is Bad state: Cannot add new events after calling close
I/flutter ( 7710): Bloc error is Bad state: Cannot add new events after calling close
Вы видите, что закрывает блок?
Большое спасибо.
AuthenticationBloc
class AuthenticationBloc
extends Bloc<AuthenticationEvent, AuthenticationState> {
final UserRepository _userRepository;
AuthenticationBloc({@required UserRepository userRepository})
: assert(userRepository != null),
_userRepository = userRepository;
StreamSubscription subscription;
String verificationId = "";
@override
AuthenticationState get initialState => Uninitialized();
@override
Stream<AuthenticationState> mapEventToState(
AuthenticationEvent event) async* {
if (event is StartApp) {
yield* _startAppToState();
}
if (event is AppStarted) {
yield* _mapAppStartedToState();
} else if (event is LoggedIn) {
yield* _mapLoggedInToState();
} else if (event is LoggedOut) {
yield* _mapLoggedOutToState();
}
// phone verification
if (event is VerifyPhoneNumberEvent) {
print('VerifyPhoneNumberEvent received');
yield VerifyingState();
subscription = _mapVerifyPhoneNumberToState(event.phoneNumber).listen((event) {
add(event);
});
} else if (event is PhoneCodeSentEvent) {
print('PhoneCodeSentEvent received');
yield OtpSentState();
} else if (event is VerificationCompletedEvent) {
print('VerificationCompletedEvent received');
yield VerificationCompleteState(firebaseUser: event.firebaseUser, isVerified: event.isVerified);
} else if (event is VerificationExceptionEvent) {
print('VerificationExceptionEvent received');
yield VerificationExceptionState(message: event.message);
} else if (event is VerifySmsCodeEvent) {
print('VerifySmsCodeEvent received');
yield VerifyingState();
try {
AuthResult result =
await _userRepository.verifyAndLinkAuthCredentials(verificationId: verificationId, smsCode: event.smsCode);
if (result.user != null) {
yield VerificationCompleteState(firebaseUser: result.user, isVerified: true);
} else {
yield OtpExceptionState(message: "Invalid otp!",verificationId: verificationId);
}
} catch (e) {
yield OtpExceptionState(message: "Invalid otp!", verificationId: verificationId);
print(e);
}
} else if ( event is PhoneCodeAutoRetrievalTimeoutEvent){
yield PhoneCodeAutoRetrievalTimeoutState(verificationId: event.verificationId);
}
if(event is SendVerificationCodeEvent) {
yield*_mapVerificationCodeToState(event);
}
}
Stream<AuthenticationEvent> _mapVerifyPhoneNumberToState(String phoneNumber) async* {
print('_mapVerifyPhoneNumberToState started');
StreamController<AuthenticationEvent> phoneVerificationStreamController = StreamController();
final phoneVerificationCompleted = (AuthCredential authCredential) {
print('_mapVerifyPhoneNumberToState PhoneVerificationCompleted');
// _userRepository.getUser();
_userRepository.getCurrentUser().catchError((onError) {
print(onError);
}).then((user) {
phoneVerificationStreamController.add(VerificationCompletedEvent(firebaseUser: user, isVerified: true));
// phoneVerificationStreamController.close();
});
};
final phoneVerificationFailed = (AuthException authException) {
print('_mapVerifyPhoneNumberToState PhoneVerificationFailed');
print(authException.message);
phoneVerificationStreamController.add(VerificationExceptionEvent(onError.toString()));
// phoneVerificationStreamController.close();
};
final phoneCodeSent = (String verificationId, [int forceResent]) {
print('_mapVerifyPhoneNumberToState PhoneCodeSent');
this.verificationId = verificationId;
phoneVerificationStreamController.add(PhoneCodeSentEvent());
};
final phoneCodeAutoRetrievalTimeout = (String verificationId) {
// after this print Bloc error is Bad state: Cannot add new events after calling close
print('_mapVerifyPhoneNumberToState PhoneCodeAutoRetrievalTimeout');
this.verificationId = verificationId;
// phoneVerificationStreamController.close();
// phoneVerificationStreamController.add(PhoneCodeAutoRetrievalTimeoutEvent(verificationId: verificationId));
};
await _userRepository.verifyPhone(
phoneNumber: phoneNumber,
timeOut: Duration(seconds: 0), // 0 triggers PhoneCodeAutoRetrievalTimeout immediately
phoneVerificationFailed: phoneVerificationFailed,
phoneVerificationCompleted: phoneVerificationCompleted,
phoneCodeSent: phoneCodeSent,
autoRetrievalTimeout: phoneCodeAutoRetrievalTimeout);
yield* phoneVerificationStreamController.stream;
}
Stream<AuthenticationState> _startAppToState() async* {
Timer(Duration(seconds: 5), () {
add(AppStarted());
});
}
Stream<AuthenticationState> _mapAppStartedToState() async* {
try {
final isSignedIn = await _userRepository.isSignedIn();
if (isSignedIn) {
final user = await _userRepository.getUser();
yield Authenticated(user);
} else {
yield Unauthenticated();
}
} catch (_) {
yield Unauthenticated();
}
}
Stream<AuthenticationState> _mapLoggedInToState() async* {
yield Authenticated(await _userRepository.getUser());
}
Stream<AuthenticationState> _mapLoggedOutToState() async* {
yield Unauthenticated();
_userRepository.signOut();
}
Stream<AuthenticationState> _mapVerificationCodeToState(SendVerificationCodeEvent event) async* {
print('_mapVerificationCodeToState started');
yield VerifyingState();
try {
AuthResult result =
await _userRepository.verifyAndLinkAuthCredentials(verificationId: verificationId, smsCode: event.smsCode);
if (result.user != null) {
yield VerificationCompleteState(firebaseUser: result.user, isVerified: true);
} else {
yield OtpExceptionState(message: "Invalid otp!", verificationId: verificationId);
}
} catch (e) {
yield OtpExceptionState(message: "Invalid otp!", verificationId: verificationId);
print(e);
}
}
}
AuthenticationEvent:
class VerifyPhoneNumberEvent extends AuthenticationEvent {
final String phoneNumber;
VerifyPhoneNumberEvent({this.phoneNumber});
}
class VerifySmsCodeEvent extends AuthenticationEvent {
final String smsCode;
VerifySmsCodeEvent({this.smsCode});
}
class PhoneCodeSentEvent extends AuthenticationEvent {}
class VerificationCompletedEvent extends AuthenticationEvent {
final FirebaseUser firebaseUser;
final bool isVerified;
VerificationCompletedEvent({@required this.firebaseUser, @required this.isVerified});
@override
List<Object> get props => [firebaseUser,isVerified];
@override
String toString() => 'VerificationCompleteEvent{user:${firebaseUser.displayName}, isVerified: $isVerified}';
}
class VerificationExceptionEvent extends AuthenticationEvent {
final String message;
VerificationExceptionEvent(this.message);
}
class PhoneCodeAutoRetrievalTimeoutEvent extends AuthenticationEvent {
final String verificationId;
PhoneCodeAutoRetrievalTimeoutEvent({@required this.verificationId});
@override
List<Object> get props => [verificationId];
@override
String toString() => 'PhoneCodeAutoRetrievalTimeoutEvent {verificationId: $verificationId}';
}
AuthenticationState:
class OtpSentState extends AuthenticationState {}
class VerifyingState extends AuthenticationState {}
class OtpVerifiedState extends AuthenticationState {}
class PhoneCodeAutoRetrievalTimeoutState extends AuthenticationState {
final String verificationId;
PhoneCodeAutoRetrievalTimeoutState({@required this.verificationId});
@override
List<Object> get props => [verificationId];
@override
String toString() => 'PhoneCodeAutoRetrievalTimeoutState {verificationId: $verificationId}';
}
class VerificationCompleteState extends AuthenticationState {
final FirebaseUser firebaseUser;
final bool isVerified;
VerificationCompleteState({@required this.firebaseUser, @required this.isVerified});
FirebaseUser getUser(){
return firebaseUser;
}
@override
List<Object> get props => [firebaseUser, isVerified];
@override
String toString() => 'VerificationCompleteState{user:${firebaseUser.displayName}, isVerified: $isVerified}';
}
class VerificationExceptionState extends AuthenticationState {
final String message;
VerificationExceptionState({this.message});
@override
// TODO: implement props
List<Object> get props => [message];
}
class OtpExceptionState extends AuthenticationState {
final String message;
final String verificationId;
OtpExceptionState({@required this.message, @required this.verificationId});
@override
// TODO: implement props
List<Object> get props => [message, verificationId];
}
Платежный экран:
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<AuthenticationBloc>(
create: (context) => AuthenticationBloc(userRepository: UserRepository()),
lazy: false,
),
BlocProvider<UserBloc>(
create: (context) => UserBloc(),
lazy: false,
),
BlocProvider<BookingBloc>(
create: (context) => BookingBloc(user: widget.user),
),
BlocProvider<OrderBloc>(
create: (context) => OrderBloc(user: widget.user),
),
BlocProvider<PaymentBloc>(
create: (context) => PaymentBloc(user: widget.user),
lazy: false,
),
BlocProvider<CartBloc>(
create: (context) => CartBloc()..add(LoadCart()),
),
],
child:
BlocBuilder<PaymentBloc, PaymentState>(builder: (context, state) {
if (state is InitialStatePayment) {
return MultiBlocListener(
listeners: [
BlocListener<AuthenticationBloc, AuthenticationState>(
listener: (BuildContext context, AuthenticationState state){
// ain't no sunshine
if (state is VerificationExceptionState ) {
scaffoldKey.currentState.showSnackBar(SnackBar(
backgroundColor: Colors.redAccent,
content: Text(
AppLocalizations.instance
.text('Phone verification error'),
style: TextStyle(color: Colors.white))));
}
//manually insert OTP
if (state is PhoneCodeAutoRetrievalTimeoutState) {
print('PhoneCodeAutoRetrievalTimeoutState');
// setState(() {
controller.text = null;
// });
// Navigator.of(context,rootNavigator: false).pop(context);
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context){
return VerifyOtpDialog(
controller: controller,
onPressed: (){
if (controller.text.length == 6) {
// Navigator.of(context,rootNavigator: false).pop(context);
BlocProvider.of<AuthenticationBloc>(context).add(SendVerificationCodeEvent(verificationId: state.verificationId, smsCode: controller.text.replaceAll(' ', '')));
} else {
scaffoldKey.currentState.showSnackBar(SnackBar(
backgroundColor: Colors.redAccent,
content: Text(
AppLocalizations.instance
.text('Wrong code'),
style: TextStyle(color: Colors.white))));
}
}
);
}
);
}
// if at the first you don't succeed..
if (state is OtpExceptionState) {
// setState(() {
controller.text = null;
// });
// Navigator.of(context,rootNavigator: false).pop(context);
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context){
return VerifyOtpRetryDialog(
controller: controller,
onPressed: (){
if (controller.text.length == 6) {
// Navigator.of(context,rootNavigator: false).pop();
BlocProvider.of<AuthenticationBloc>(context).add(SendVerificationCodeEvent(verificationId: state.verificationId, smsCode: controller.text.replaceAll(' ', '')));
} else {
scaffoldKey.currentState.showSnackBar(SnackBar(
backgroundColor: Colors.redAccent,
content: Text(
AppLocalizations.instance
.text('Wrong code'),
style: TextStyle(color: Colors.white))));
}
}
);
}
);
}
// kool and the gang
if (state is VerificationCompleteState) {
if (state.isVerified == true) {
// setState(() {
isVerified = state.isVerified;
// });
// Navigator.of(context,rootNavigator: false).pop(context);
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context){
return VerifiedPhoneConfirmationDialog();
}
);
Timer(Duration(milliseconds: 1200), (){
// Navigator.of(context,rootNavigator: false).pop();
});
// TODO: Save user isVerified to LocalDb and Firebase
}
}
}
),
...
диалоговое окно, запускающее проверку телефона:
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context){
return SingleChildScrollView(
child: ValidatePhoneDialog(
controller: controller,
onPressed: (){
if (controller.text.length >= 9){
// Navigator.pop(context);
showDialog(
context:context,
barrierDismissible: false,
builder: (BuildContext context){
return VerifyingDialog();
}
);
BlocProvider.of<AuthenticationBloc>(context).add(VerifyPhoneNumberEvent(phoneNumber: controller.text.replaceAll(' ', '')));
} else {
scaffoldKey.currentState.showSnackBar(SnackBar(
backgroundColor: Colors.redAccent,
content: Text(
AppLocalizations.instance
.text('Wrong number'),
style: TextStyle(color: Colors.white))));
}
}
),
);
}
);