Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Browser back button is acting weird and not adhering to the VGaurds #184

Open
muezz opened this issue Mar 28, 2022 · 0 comments
Open

Browser back button is acting weird and not adhering to the VGaurds #184

muezz opened this issue Mar 28, 2022 · 0 comments

Comments

@muezz
Copy link

muezz commented Mar 28, 2022

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

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

issue_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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant