Flutter initial state Cubit - flutter

When start the app, first work AuthCubit
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<AuthCubit>(
create: (context) => getIt<AuthCubit>()..startApp()),
BlocProvider<CheckRoleCubit>(
create: (context) => getIt<CheckRoleCubit>()),
],
child: MaterialApp(
home: InitialScreen(),
),
);
}
}
which use the simple functions:
void startApp() async {
emit(AuthState.splashScreen());
await Future.delayed(Duration(seconds: 2));
checkAuthRequest();
}
Future<void> checkAuthRequest() async {
final userOption = await firebaseAuthRepository.getUser();
emit(
userOption.fold(
() => AuthState.unauthenticated(),
(_) => AuthState.authenticated(),
),
);
}
in InitialScreen I am using BlocConsumer:
return BlocConsumer<AuthCubit, AuthState>(
listener: (context, state) {
state.maybeMap(
orElse: () {},
authenticated: (_) {
context.read<CheckRoleCubit>().checkRole();
},
);
},
builder: (context, state) {
return state.maybeMap(
authenticated: (_) => CheckRole(),
unauthenticated: (_) => HomeScreen(),
orElse: () => Container(),
);
},
);
in CheckRoleScreen:
return BlocBuilder<CheckRoleCubit, CheckRoleState>(
builder: (context, state) {
return state.map(
initial: (_) => Container(color: Colors.amberAccent),
admin: (_) => Admin(),
user: (_) => HomeScreen(),
loadFailure: (_) => Container(),
);
},
);
in CheckRoleCubit I am creating a simple function, which fetch userData from Firestore.
StreamSubscription userDataStreamSubscription;
Future<void> checkRole() async {
userDataStreamSubscription = userDataRepository.fetchUserData().listen(
(failureOrFetch) {
emit(
failureOrFetch.fold(
(failure) => CheckRoleState.loadFailure(failure),
(userData) {
if (userData.role == 'admin') {
return CheckRoleState.admin();
} else {
return CheckRoleState.user();
}
},
),
);
},
);
}
Issue is when I am open the app or use restart, after splashScreen, emit initial state of CheckRoleCubit and display container for a second and then display homeScreen or adminScreen. What I am doing wrong?

Related

Error handling Flutter Web x go_router x FirebaseAuth (EmailLink)

I am trying to load make a dashboard and now developing the login site. It works that the user gets and email but when I click on the link provided in the email, the "FirebaseAuth.instance.isSignInWithEmailLink($link)" returns false, because $link is "localhost:8080/login" (the current page) instead of the link that has been sent via email.
Here is the FirebaseAuthService code:
class FirebaseAuthService implements AuthService {
FirebaseAuthService() {
_initialize();
}
Future<void> _initialize() async {
/// Set auth persistance for web so user stays signed in
await FirebaseAuth.instance.setPersistence(Persistence.LOCAL);
print('debug// window.location.href: ' + window.location.href);
print('debug// Uri.base.toString(): ' + Uri.base.toString());
print('debug2// window.localStorage[email]: ' + window.localStorage['email'].toString());
/// idk man...
FirebaseAuth.instance.authStateChanges().listen((User? firebaseUser) {
if (firebaseUser == null) {
print('User is currently signed out!');
} else {
print('User is signed in!');
}
});
/// Checks if the incoming link is the OTP email link.
// if (FirebaseAuth.instance.isSignInWithEmailLink(Uri.base.toString())) {
if (FirebaseAuth.instance.isSignInWithEmailLink(window.location.href)) {
print('in method debug2// window.location.href: ' + window.location.href);
print('in method debug2// html.window.document.referrer: ' + (window.document as HtmlDocument).referrer);
print('in method debug// Uri.base.toString(): ' + Uri.base.toString());
print('in method debug2// window.localStorage[email]: ' + window.localStorage['email'].toString());
if (kDebugMode) print('Trying to sign in the user with OTP');
try {
await FirebaseAuth.instance
.signInWithEmailLink(
email: window.localStorage['email'] ?? '',
emailLink: window.location.href,
)
.timeout(const Duration(seconds: 10))
.then((value) => print('value: ${value.toString()}'));
} catch (_) {
print('Exceptino.... $_');
}
window.localStorage.remove('email');
if (kDebugMode) print('Successfully signed in the user with OTP');
}
}
#override
bool get isSignedIn => FirebaseAuth.instance.currentUser != null;
#override
Future<void> signOut() async {
await FirebaseAuth.instance.signOut().timeout(const Duration(seconds: 10));
}
}
And here is my main class where FirebaseAuthService is provided (with the provider package):
class VamosEventsDashboard extends StatelessWidget {
VamosEventsDashboard();
final GoRouter _vamosRouter = GoRouter(
debugLogDiagnostics: true,
initialLocation: EventsPage.route,
errorBuilder: (_, __) => const ErrorPage(),
routes: [
GoRoute(path: EventsPage.route, builder: (_, __) => const EventsPage()), // events
GoRoute(path: LoginPage.route, builder: (_, __) => const LoginPage()), // login
],
redirect: (BuildContext context, GoRouterState state) {
return context.watch<AuthService>().isSignedIn ? EventsPage.route : LoginPage.route; // todo change back to events
},
);
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
// Data sources and services
Provider<OrganizationDataSource>(create: (_) => const FirestoreDataSource()),
Provider<AuthService>(create: (_) => FirebaseAuthService()),
],
child: MultiProvider(
providers: [
// View models
ChangeNotifierProvider(
create: (context) => OrganizationViewModel(organizationDataSource: context.read<OrganizationDataSource>()),
),
ChangeNotifierProvider(create: (_) => LoginViewModel()),
],
child: MaterialApp.router(
theme: vamosTheme,
routerConfig: _vamosRouter,
title: 'vamos! Events Dashboard',
),
),
);
}
}

I am trying to prevent unauthenticated users from navigating to a route using a URL in a flutter web app. But I have problems with providers

So I am using this answer Prevent unauthenticated users from navigating to a route using a URL in a flutter web app? to make my task. But I always have this error: Error: Could not find the correct Provider<UserProvider> above this Consumer<UserProvider> Widget.
My main.dart:
import '/providers/user_provider.dart';
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final Future<FirebaseApp> _initialization = Firebase.initializeApp();
return FutureBuilder(
future: _initialization,
builder: (context, appSnapshot) {
return MaterialApp(
//smth
onUnknownRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
settings: settings,
builder: (BuildContext context) => NotFoundErrorScreen(),
);
},
routes: {
AuthScreen.routeName: (context) => AuthScreen(),
DashboardScreen.routeName: (context) => DashboardScreen(),
ProductsScreen.routeName: (context) => ProductsScreen(),
PermissionErrorScreen.routeName: (context) => PermissionErrorScreen(),
},
builder: (context, child) {
return Consumer<UserProvider>(
child: DashboardScreen(),
builder: (context, provider, child) {
if (provider.isLoading) {
return SplashScreen();
}
final value = provider.user;
if (value == null) {
return Navigator(
onGenerateRoute: (settings) => MaterialPageRoute(
settings: settings, builder: (context) => AuthScreen()),
);
}
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => UserProvider()),
],
child: child,
);
},
);
},
);
},
);
}
}
My user_provider.dart:
class UserProvider with ChangeNotifier {
bool _isLoading = true;
Map<String, dynamic> _user;
bool get isLoading => _isLoading;
Map<String, dynamic> get user => _user;
getUser() async {
FirebaseAuth.instance.authStateChanges().listen((currentUser) async {
_isLoading = false;
if (currentUser == null) {
_user = null;
notifyListeners();
return;
}
final data = await FirebaseFirestore.instance
.collection('users')
.doc(currentUser.uid)
.snapshots()
.first;
if (data.exists) {
print(data.data());
if (data.data()['role'] == 'admin') {
_user = (data.data());
notifyListeners();
}
return null;
}
});
}
}
This is my first experience with flutter web, so I am confused.
My first thaught was that it does not work, because I don't invoke my getUser() function, however I do not know how to fix it.
Thank you in advance.

Flutter Cubit, Listen to other Cubit states

I tried to implement a FilterEventCubit that listens to the states of a LocationTrackerCubit and an EventLoaderCubit. I used the tutorial from Felix Angelov (https://bloclibrary.dev/#/fluttertodostutorial) as a template:
class EventFilterCubit extends Cubit<EventFilterState> {
final EventLoaderCubit eventLoaderCubit;
final UserCubit userCubit;
final LocationTrackerCubit locationTrackerCubit;
StreamSubscription? eventsSubscription;
StreamSubscription? locationSubscription;
EventFilterCubit(
this.eventLoaderCubit, this.userCubit, this.locationTrackerCubit)
: super(
eventLoaderCubit.state is EventsUpToDate &&
userCubit.state is UserUpToDate &&
// TODO: not working correctly
locationTrackerCubit.state is LocationLoadSuccess
? EventFilterState.filteredEventsLoadSuccess(
(eventLoaderCubit.state as EventsUpToDate).events,
EventFilter.initial(
LatLng(
(locationTrackerCubit.state as LocationLoadSuccess)
.location
.latitude,
(locationTrackerCubit.state as LocationLoadSuccess)
.location
.longitude,
),
),
)
: const EventFilterState.filteredEventsLoadInProgress(),
) {
eventsSubscription = eventLoaderCubit.stream.listen(
(state) {
if (state is EventsUpToDate) {
print("Eventloaderbloc updated events");
eventsUpdated((eventLoaderCubit.state as EventsUpToDate).events);
}
},
);
locationSubscription = locationTrackerCubit.stream.listen(
(state) {
if (state is LocationLoadSuccess) {
print("locationtrackercubit updated location");
locationUpdated(
(locationTrackerCubit.state as LocationLoadSuccess).location,
);
}
},
);
}
The EventFilterCubit then builds the feed with all the events.
When I build the app or do a hot restart everything works just fine. But it stops listening after the first state update, so anytime a new event is added or the events are reloaded, nothing happens in the UI and the updateEvents function is not triggered.
Here is also my EventLoaderCubit:
part 'event_loader_cubit.freezed.dart';
part 'event_loader_state.dart';
#injectable
class EventLoaderCubit extends Cubit<EventLoaderState> {
final IEventRepository eventRepository;
// TODO: Streams atm useless
final StreamController<List<Event>> _eventController =
StreamController<List<Event>>();
Stream<List<Event>> get eventStream => _eventController.stream;
EventLoaderCubit(this.eventRepository)
: super(
const EventLoaderState.loadInProgress(),
) {
refreshEvents();
}
Future<void> refreshEvents() async {
print("refresh events");
emit(const EventLoaderState.loadInProgress());
final eventsFailOrSuccess = await eventRepository.loadEvents();
eventsFailOrSuccess.fold(
(failure) => emit(
const EventLoaderState.loadFailure(
EventFailure.serverError(),
),
),
(events) {
_eventController.add(events);
// getIt<EventFilterCubit>().eventsUpdated(events);
emit(
EventLoaderState.eventsUpToDate(events),
);
},
);
}
}
And the provider initialization:
child: BlocProvider(
create: (context) => getIt<AuthCubit>()..initialized(),
child: BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) {
if (state is Authenticated) {
return MultiBlocProvider(
providers: [
BlocProvider<LocationTrackerCubit>(
create: (context) => getIt<LocationTrackerCubit>(),
),
BlocProvider<UserCubit>(
create: (context) => getIt<UserCubit>(),
),
BlocProvider<EventLoaderCubit>(
create: (context) =>
// TODO: Doesn't execute refreshEvents()
getIt<EventLoaderCubit>(),
),
BlocProvider<EventFilterCubit>(
create: (context) => getIt<EventFilterCubit>(),
),
],
child: _materialApp(context, authedRouter),
Also, as it is written in the code, the refreshEvents() function doesn't run when the Cubit is injected.
I had also troubles with the getIt dependency injection and BloCs. I solved this by changing the bloc access to the context.read and now it works fine.
You could try something like:
return MultiBlocProvider(
providers: [
BlocProvider<UserCubit>(
lazy: false,
create: (context) => UserCubit(
getIt<IUserRepository>(),
),
),
BlocProvider<LocationTrackerCubit>(
lazy: false,
create: (context) => LocationTrackerCubit(),
),
BlocProvider<EventLoaderCubit>(
lazy: false,
create: (context) => EventLoaderCubit(
getIt<IEventRepository>(),
),
),
BlocProvider<EventFilterCubit>(
create: (context) => EventFilterCubit(
eventLoaderCubit:
BlocProvider.of<EventLoaderCubit>(context),
userCubit: BlocProvider.of<UserCubit>(context),
locationTrackerCubit:
BlocProvider.of<LocationTrackerCubit>(context),
),
),
],

Using local authentication inside Future builder is calling local authentication infinite number of times after authenticating

I am trying to use local authentication to authenticate the user before he uses the app . But the problem is that I have to use Future Builder for checking user data to go to Home Screen or Login Screen According. Therefore, I have to use local authentication inside Future Builder to authenticate user. But this results in calling fingerprint auth infinite times after I reach to home screen also. So We can't get rid of local auth. Please help and tell if there is another way around . Thanks In Advance :)
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final AuthMethods _authMethods = AuthMethods();
final LocalAuthentication _localAuthentication = LocalAuthentication();
bool _hasFingerPrintSupport = false;
bool _authorizedOrNot = false;
List<BiometricType> _availableBuimetricType = List<BiometricType>();
#override
void initState() {
super.initState();
_getBiometricsSupport();
_getAvailableSupport();
}
Future<void> _getBiometricsSupport() async {
bool hasFingerPrintSupport = false;
try {
hasFingerPrintSupport = await _localAuthentication.canCheckBiometrics;
} catch (e) {
print(e);
}
if (!mounted) return;
setState(() {
_hasFingerPrintSupport = hasFingerPrintSupport;
});
}
Future<void> _getAvailableSupport() async {
List<BiometricType> availableBuimetricType = List<BiometricType>();
try {
availableBuimetricType =
await _localAuthentication.getAvailableBiometrics();
} catch (e) {
print(e);
}
if (!mounted) return;
setState(() {
_availableBuimetricType = availableBuimetricType;
});
}
Future<void> _authenticateMe() async {
bool authenticated = false;
try {
authenticated = await _localAuthentication.authenticateWithBiometrics(
localizedReason: "Authenticate to use App", // message for dialog
useErrorDialogs: true,// show error in dialog
stickyAuth: false,// native process
);
} catch (e) {
print(e);
}
if (!mounted) return;
setState(() {
_authorizedOrNot = authenticated ? true : false;
});
}
#override
Widget build(BuildContext context) {
final themeNotifier = Provider.of<ThemeNotifier>(context);
_authenticateMe();
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => ThemeNotifier(darkTheme),
),
ChangeNotifierProvider(create: (_) => ImageUploadProvider()),
ChangeNotifierProvider(
create: (_) => VideoUploadProvider(),
),
ChangeNotifierProvider(create: (_) => UserProvider()),
],
child: MaterialApp(
title: "App",
debugShowCheckedModeBanner: false,
initialRoute: '/',
routes: {
'/search_screen': (context) => SearchScreen(),
'/setting_page': (context) => settingPage(),
},
theme: themeNotifier.getTheme(),
home: FutureBuilder(
future: _authMethods.getCurrentUser(),
builder: (context, AsyncSnapshot<User> snapshot) {
if (snapshot.hasData ) {
return _authorizedOrNot==true ? HomeScreen() : Container();
} else {
return LoginScreen();
}
},
),
),
);
}
}
In this particular case you call _authenticateMe(); at the beginning of your build().
_authenticateMe(); has inside a setState that cause build() to refire again and call _authenticateMe(); thus rebuilding thus rebuilding.
P.S. I would move the FutureBuilder up until is over the MaterialApp, it may cause problem with the use of the hot reload.
Well I figured out a way around by calling the authenticate function in init state and then checking for isauthorizedorNot before returning Future builder .
Here is the code :-
class _MyAppState extends State<MyApp> {
final LocalAuthentication _localAuthentication = LocalAuthentication();
final AuthMethods _authMethods = AuthMethods();
bool _authorizedOrNot ;
Future<void> _authenticateMe() async {
bool authenticated = false;
try {
authenticated = await _localAuthentication.authenticateWithBiometrics(
localizedReason: "Authenticate to use app",
useErrorDialogs: true,
stickyAuth: false,
);
} catch (e) {
print(e);
}
if (!mounted) return;
setState(() {
_authorizedOrNot = authenticated ? true : false;
});
}
#override
void initState() {
super.initState();
_authenticateMe();
}
#override
Widget build(BuildContext context) {
final themeNotifier = Provider.of<ThemeNotifier>(context);
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => ThemeNotifier(darkTheme),
),
ChangeNotifierProvider(create: (_) => ImageUploadProvider()),
ChangeNotifierProvider(
create: (_) => VideoUploadProvider(),
),
ChangeNotifierProvider(create: (_) => UserProvider()),
],
child: MaterialApp(
title: "App",
debugShowCheckedModeBanner: false,
initialRoute: '/',
routes: {
'/search_screen': (context) => SearchScreen(),
'/setting_page': (context) => settingPage(),
},
theme: themeNotifier.getTheme(),
home: _authorizedOrNot==true ? FutureBuilder(
future: _authMethods.getCurrentUser(),
builder: (context, AsyncSnapshot<User> snapshot) {
if (snapshot.hasData) {
return HomeScreen();
} else {
return LoginScreen();
}
},
) : ( Container(child: Center(child: CircularProgressIndicator()),)
),)
);
}
}

How to delay returning a screen in BlocBuilder Flutter

In my app I use flutter_bloc for state management and in main() I use a BlocBuilder for the Authentication which if it receive an Authenticated state returns MapScreen, if state is Unauthenticated it returns the LoginScreen, else returns the Splashscreen. I'd like to control how long Splashscreen is displayed so I tried adding a timer in BlocBuilder inside state checking but it never returns the screen. How would I set Splashscreen stay visible for a certain amount of time?
As always many thanks for your time and help.
This is the BlocBuilder:
home: BlocBuilder<AuthenticationBloc, AuthenticationState>(
builder: (context, state) {
if (state is Unauthenticated) {
// Timer(Duration(seconds: 10), () {
return LoginScreen(userRepository: _userRepository);
// });
}
if (state is Authenticated) {
// Timer(Duration(seconds: 50), () {
return MultiBlocProvider(
providers: [
BlocProvider<DefaultsBloc>(
lazy: false,
create: (context) => DefaultsBloc()..add(InitializeRemote()),
),
BlocProvider<TrackingBloc>(
create: (context) => TrackingBloc(),
),
BlocProvider<DirectionsBloc>(
create: (context) => DirectionsBloc(),
),
BlocProvider<GeoBloc>(
create: (context) => GeoBloc(),
),
BlocProvider<RouteBloc>(
lazy: false,
create: (context) => RouteBloc(),
),
BlocProvider<SchedulerBloc>(
create: (context) => SchedulerBloc(),
),
BlocProvider<CheckerBloc>(
create: (context) => CheckerBloc(),
),
BlocProvider<LocationBloc>(
lazy: false,
create: (context) => LocationBloc(
mapRepository: _mapRepository,
)
..add(GetLocationStream())
..add(GetLocation())
..add(GetIsoLocationUser())),
BlocProvider<AlertBloc>(
create: (context) => AlertBloc(
alertRepository: _alertRepository, user: state.user),
),
BlocProvider<LocalNotificationBloc>(
lazy: false,
create: (context) => LocalNotificationBloc(),
)
],
child: MapScreen(
// mapRepository: _mapRepository,
user: state.user,
// alertRepository: FirebaseAlertRepository(),
),
);
// });
}
return SplashScreen();
},
),
I had to create a new event StartApp to be the first sent to bloc and then in bloc I set a timer to add the AppStartedevent that starts all the authentication logic.
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();
}
}
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();
}
}
I hope this will help others.
Cheers.
You can't use a timer in the build method. You can create a new StatefulWidget and then add a timer in initState whcih navigates to the next screen, which will be the widget you currently have used for home.
import 'dart:async';
import 'package:flutter/material.dart';
class Splash extends StatefulWidget {
#override
_SplashState createState() => _SplashState();
}
class _SplashState extends State<Splash> {
#override
void initState() {
super.initState();
Timer(
const Duration(seconds: 1),
() => Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => OtherScreen()),
),
);
}
#override
Widget build(BuildContext context) {
return Material(
child: Center(
child: Text('Splash'),
),
);
}
}