vrouter
vrouter copied to clipboard
Browser back button is acting weird and not adhering to the VGaurds
I am trying to implement VGuard
from the VRouter
package with the help of Riverpod as State Notifier. In the case, the states are loading
, unauthenticated
and authenticated
.
Below you will find two GIFs with their respective debug console outputs and a table with the list of actions taken and their respective results. The first GIF shows everything working as it should. The second GIF shows a unique case where it is not working.
In the end you will find the code for each class in order to recreate this issue.
GIF 01
Action | Outcome | Result |
---|---|---|
App Starts | Login Shown | Expected |
Dashboard URL Entered | Redirected to Login | Expected |
Tab Reloaded | Back to Login | Expected |
Login Button Pressed | Dashboard Shown | Expected |
Login URL Entered | Redirected to Dashboard | Expected |
Logout Button Pressed | Taken to Login Page | Expected |
Dashboard URL Entered | Redirected to Login | Expected |
Browser Back Button Pressed Several Times | Redirected to Login Page | Expected |
Tab Reloaded | Login Page Shown | Expected |
Dashboard URL Entered | Redirected to Login | Expected |
GIF 02
Action | Outcome | Result |
---|---|---|
App Starts | Login Shown | Expected |
Login Button Pressed | Dashboard Shown | Expected |
Login URL Entered | Redirected to Dashboard | Expected |
Browser Back Button Pressed (1st) | Dashboard Shown | Expected |
Browser Back Button Pressed (2nd) | Dashboard Shown but with URL of Login | Not Expected |
Login URL Entered | Login Shown | Not Expected |
Dashboard URL Entered | Dashboard Shown | Not Expected |
Login URL Entered | Login Shown | Not Expected |
Dashboard URL Entered | Dashboard Shown | Not Expected |
Logout Button Pressed | Taken to Login Page | Expected |
Dashboard URL Entered | Redirected to Login | Expected |
main.dart
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const ProviderScope(child: FranchiseManager()));
}
class MyApp extends ConsumerWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return VRouter(
debugShowCheckedModeBanner: false,
//mode: VRouterMode.history,
initialUrl: '/login',
routes: [
VGuard(
beforeEnter: (vRedirector) async {
final _authChangeTest = ref.read(authStateNotifier);
debugPrint('VGaurd was called: ' + _authChangeTest.status.name);
if (_authChangeTest.status == AuthStatus.unauthenticated) {
vRedirector.to('/login');
}
},
stackedRoutes: [
myVRoutes['/dashboard']!,
],
),
VGuard(
beforeEnter: (vRedirector) async {
final _authChangeTest = ref.read(authStateNotifier);
debugPrint('VGaurd was called: ' + _authChangeTest.status.name);
if (_authChangeTest.status == AuthStatus.authenticated) {
vRedirector.to('/dashboard');
}
},
stackedRoutes: [
myVRoutes['/login']!,
],
),
],
);
}
}
Map<String, VRouteElement> myVRoutes = {
'/login': VWidget(path: '/login', widget: const LoginPage()),
'/dashboard': VWidget(path: '/dashboard', widget: const DashboardPage()),
};
model_user.dart
class UserModel extends Equatable {
final String uid;
final String emailAddress;
const UserModel({
required this.uid,
required this.emailAddress,
});
Map<String, dynamic> toMap() {
return {
'uid': uid,
'emailAddress': emailAddress,
};
}
factory UserModel.fromMap(Map<String, dynamic> map) {
return UserModel(
uid: map['uid'] ?? '-',
emailAddress: map['emailAddress'] ?? '-',
);
}
static const empty = UserModel(uid: '-', emailAddress: '-');
@override
String toString() => 'UserModel(uid: $uid, emailAddress: $emailAddress)';
@override
List<Object> get props => [uid, emailAddress];
}
service_auth.dart
class AuthService {
final FirebaseAuth _firebaseAuth;
AuthService(this._firebaseAuth);
UserModel get currentUser => UserModel(
emailAddress: _firebaseAuth.currentUser!.email.toString(),
uid: _firebaseAuth.currentUser!.uid.toString(),
);
Stream<User?> get authChangeStream => _firebaseAuth.authStateChanges();
Future<UserModel> loginWithEmail({
required String email,
required String password,
}) async {
try {
final loginResponse = await _firebaseAuth.signInWithEmailAndPassword(
email: email, password: password);
return UserModel(
uid: loginResponse.user!.uid,
emailAddress: loginResponse.user!.email!,
);
} on FirebaseAuthException {
return UserModel.empty;
}
}
Future<void> logOut() async {
await _firebaseAuth.signOut();
}
}
auth_notifier.dart
enum AuthStatus { loading, authenticated, unauthenticated }
class AuthState extends Equatable {
const AuthState._(
{this.user = UserModel.empty, this.status = AuthStatus.loading});
const AuthState.loading() : this._();
const AuthState.authenticated(UserModel user)
: this._(user: user, status: AuthStatus.authenticated);
const AuthState.unauthenticated()
: this._(status: AuthStatus.unauthenticated);
final UserModel user;
final AuthStatus status;
@override
List<Object?> get props => [user, status];
}
class AuthNotifier extends StateNotifier<AuthState> {
final AuthService _authService;
AuthNotifier(AuthService authService)
: _authService = authService,
super(const AuthState.loading()) {
checkUserAuth();
}
Future<void> checkUserAuth() async {
/// this is where you can check if you have the cached token on the phone
/// on app startup
/// for now we assume no such caching is done
if (await _authService.authChangeStream.any((element) => element == null)) {
state = const AuthState.unauthenticated();
} else {
state = AuthState.authenticated(_authService.currentUser);
}
}
Future<void> loginUser(String username, String password) async {
state = const AuthState.loading();
UserModel user =
await _authService.loginWithEmail(email: username, password: password);
if (user == UserModel.empty) {
state = const AuthState.unauthenticated();
} else {
/// do your pre-checks about the user before marking the state as
/// authenticated
state = AuthState.authenticated(user);
}
}
Future<void> logoutUser() async {
await _authService.logOut();
state = const AuthState.unauthenticated();
}
}
provider.dart
final _authInstance = Provider<FirebaseAuth>(
(ref) => FirebaseAuth.instance,
);
final authServiceProvider =
Provider<AuthService>((ref) => AuthService(ref.watch(_authInstance)));
final authStateNotifier = StateNotifierProvider<AuthNotifier, AuthState>(
(ref) => AuthNotifier(ref.watch(authServiceProvider)));
page_login.dart
class LoginPage extends StatelessWidget {
const LoginPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) {
final authViewModel = ref.watch(authStateNotifier.notifier);
return ElevatedButton(
onPressed: () async {
await authViewModel.loginUser(
'[email protected]', 'test_user');
context.vRouter.to('/dashboard');
},
child: const Text('Login'),
);
},
),
),
);
}
}
page_dashboard.dart
class DashboardPage extends StatelessWidget {
const DashboardPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) {
final authViewModel = ref.watch(authStateNotifier.notifier);
return IconButton(
padding: EdgeInsets.all(MyConstants.kDefaultPadding),
onPressed: () async {
await authViewModel.logoutUser();
context.vRouter.to('/login');
},
icon: const Icon(Icons.logout));
},
)
],
title: Text('Dashboard', style: MyTextStyles.kTitle),
backgroundColor: MyColors.kWidgetColor,
automaticallyImplyLeading: false,
toolbarHeight: 70,
),
body: Container(),
);
}
}
I have been trying to figure out why this is happening and I cannot seem to fix it. At this point, it feels like a bug but I am not sure.