Error handling Flutter Web x go_router x FirebaseAuth (EmailLink) - flutter

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

Related

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.

App does not navigate to a different page when user authenticates

The issue is that my app does not navigate to another page automatically when user logs in or out.
class MyApp extends StatelessWidget {
final Future<FirebaseApp> _initialization = Firebase.initializeApp();
#override
Widget build(BuildContext context) {
return FutureBuilder(
// Initialize FlutterFire:
future: _initialization,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return StreamProvider<User>.value(
value: AuthService().user,
child: MaterialApp(home: Wrapper()),
);
}
return Center(child: CircularProgressIndicator());
},
);
}
}
class Wrapper extends StatelessWidget {
const Wrapper({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
if (user != null) {
return MaterialApp(initialRoute: '/', routes: {
'/': (context) => Home(),
'/profile': (context) => Profile()
});
}
return MaterialApp(initialRoute: '/', routes: {
'/': (context) => Welcome(),
'/signup': (context) => SignUp(),
'/signin': (context) => SignIn()
});
}
}
When the app starts it does show the Welcome() page. Then i am able to navigate to the signup page by pressing a signup button as such
onPressed: () {Navigator.pushNamed(context, "/signup");}),
but then when the user signs up, the app doesn't automatically navigate to Home()
class AuthService {
FirebaseAuth auth = FirebaseAuth.instance;
User _userFromFirebaseUser(User user) {
return user != null ? User(id: user.uid) : null;
}
Stream<User> get user {
return auth.authStateChanges().map(_userFromFirebaseUser);
}
Future<String> signUp(email, password) async {
try {
UserCredential user = await auth.createUserWithEmailAndPassword(
email: email, password: password);
await FirebaseFirestore.instance
.collection('users')
.doc(user.user.uid)
.set({'name': email, 'email': email});
_userFromFirebaseUser(user.user);
} on FirebaseAuthException catch (e) {
return e.code;
} catch (e) {
return e;
}
return "";
}
}
I am not sure what the issue is. Any help is appreciated.
First of all you need 1 MaterialApp not 3, then try to debug signUp method maybe there is an erorr for instance signUp returns Future<String> but in catch block you are returning an Exception and finally I suggest you to use Cubit whenever you need to listen state changes to navigate.

Flutter initial state Cubit

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?

Not getting provider data when navigating in flutter

I know flutter provider is scoped. So I declared providers (those will be needed everywhere) top of MaterialApp. In a screen am chaning a provider value and navigating to another screen. In that screen am not getting the data. Need suggestions and guide where I have done the mistake
main.dart
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<UserAuthViewModel>(
create: (context) => sl<UserAuthViewModel>()),
ChangeNotifierProvider<UserProfileViewModel>(
create: (context) => sl<UserProfileViewModel>()),
ChangeNotifierProvider<BottomNavViewModel>(
create: (context) => sl<BottomNavViewModel>()),
],
child: MaterialApp(
title: "Footsapp",
theme: ThemeData(fontFamily: 'Montserrat'),
debugShowCheckedModeBanner: false,
home: isFirstLaunch == true ? OnBoarding() : SplashView(),
routes: {
//onboarding
SplashView.SCREEN_ID: (context) => SplashView(),
//bottom nav pages
BottomNavContainer.SCREEN_ID: (context) => BottomNavContainer(),
},
),
),
);
splash screen where getting some info from api call
class _SplashViewState extends State<SplashView>
with TokenProvider, AfterLayoutMixin<SplashView> {
final userProfileViewModel = sl<UserProfileViewModel>();
final prefUtil = sl<SharedPrefUtil>();
#override
void afterFirstLayout(BuildContext context) {
Future.delayed(Duration(seconds: 1), () async {
bool isLoggedIn = prefUtil.readBool(IS_LOGGED_IN) ?? false;
bool initialProfileUpdated =
prefUtil.readBool(INITIAL_PROFILE_UPDATED) ?? false;
isLoggedIn == true
? getProfileInfo()
: await Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (_) => SocialLoginRegScreen(),
),
(route) => false);
});
}
#override
void initState() {
super.initState();
}
void getProfileInfo() async {
final userProfileResponse = await userProfileViewModel.getUserProfileData();
if (userProfileResponse.success == true) {
print('In splash: ${userProfileViewModel.userProfile.toString()}');
//from log
In splash: {firstName: Ashif, lastName: 123, birthday: 1990-02-03, email:
ashif123#gmail.com, preferredFoot: Left, preferredPosition: Midfielder,
numberOfTimesPlayedWeekly: 4}
Future.delayed(Duration(milliseconds: 500), () async {
await Navigator.pushReplacementNamed(
context,
BottomNavContainer.SCREEN_ID,
);
});
}
}
provider model class
class UserProfileViewModel extends BaseViewModel {
final _profileManageRepo = sl<ProfileManageRepo>();
Profile userProfile = Profile.initial();
Future<UserDataResponse> getUserProfileData() async {
final _userDataResponse = await _profileManageRepo.getUserProfileInfo();
if (_userDataResponse.success == true) {
userProfile = _userDataResponse.profile;
} else {
setState(ViewState.Error);
errorMessage = 'Please try again!';
}
return _userDataResponse;
}
Am trying to get the provider data (user profile) from Profile Screen. But it always get initial value.
class _UserProfileScreenState extends State<UserProfileScreen> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Consumer<UserProfileViewModel>(
builder: (context, viewmodel, child) {
print('In profile: ${viewmodel.userProfile.toString()}');
//but here
In profile: {firstName: , lastName: , birthday: , email: , preferredFoot:
Left, preferredPosition: , numberOfTimesPlayedWeekly: 1(default value)}
return Container(
child: ProfileCardWidget(
profile: viewmodel.userProfile,
),
);
},
);
}
}

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()),)
),)
);
}
}