Search code examples
flutternavigationflutter-webflutter-navigation

How to handle authenticated routes and their redirects after successful auth?


Flutter Web(Navigator 2.0/Router API): How to handle authenticated routes and their redirects after successful auth?

e.g. I have these kind of routes on my system

/book/xyz (authenticated user)
/author/abc/book/xyz (authenticated user)
/authentication (non-authenticated user)
/info (non-authenticated user)

If user opens this URL directly, I wanted to ask user to login first, at that time route will be redirected to..

/authentication

Once logged in, I would like to user to navigate previously opened URL if any else home..

Seems like this kind of things may help, any thoughts - how we can achieve similar things? https://stackoverflow.com/a/43171515/2145844

I have tried few samples for Navigation 2.0 / Router API, yes I can understand the concepts a bit..

Some of the references, I have looked at..

https://medium.com/flutter/learning-flutters-new-navigation-and-routing-system-7c9068155ade https://github.com/orestesgaolin/navigator_20_example https://github.com/flutter/flutter/tree/master/dev/benchmarks/test_apps/stocks


Solution

  • Here is how to do it using VRouter >=1.2

    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    import 'package:vrouter/vrouter.dart';
    
    void main() {
      runApp(BooksApp());
    }
    
    class Book {
      final String title;
      final Author author;
    
      Book(this.title, this.author);
    }
    
    class Author {
      String name;
    
      Author(this.name);
    }
    
    class AppState extends ChangeNotifier {
      bool _isAuthenticated = false;
    
      bool get isAuthenticated => _isAuthenticated;
    
      void authenticate() {
        if (isAuthenticated) return;
        _isAuthenticated = true;
        notifyListeners();
      }
    }
    
    class BooksApp extends StatelessWidget {
      final List<Book> books = [
        Book('Stranger in a Strange Land', Author('Robert A. Heinlein')),
        Book('Foundation', Author('Isaac Asimov')),
        Book('Fahrenheit 451', Author('Ray Bradbury')),
      ];
    
      @override
      Widget build(BuildContext context) {
        return ChangeNotifierProvider(
          create: (_) => AppState(),
          child: Builder(
            builder: (BuildContext context) {
              return VRouter(
                routes: [
                  VWidget(path: '/login', widget: AuthenticationScreen()),
                  VWidget(path: '/info', widget: InfoWidget()),
                  VGuard(
                    beforeEnter: (vRedirector) =>
                        authenticationCheck(context, vRedirector: vRedirector),
                    stackedRoutes: [
                      VWidget(
                        path: '/',
                        widget: BooksListScreen(books: books),
                        stackedRoutes: [
                          VWidget(
                            path: r'book/:bookId(\d+)',
                            widget: Builder(builder: (BuildContext context) {
                              return BookDetailsScreen(
                                book: books[int.parse(context.vRouter.pathParameters['bookId']!)],
                              );
                            }),
                          ),
                        ],
                      ),
                      VWidget(
                        path: '/authors',
                        widget: AuthorsListScreen(authors: books.map((e) => e.author).toList()),
                        stackedRoutes: [
                          VWidget(
                            path: r'/author/:authorId(\d+)',
                            widget: Builder(builder: (BuildContext context) {
                              return AuthorDetailsScreen(
                                author: books[int.parse(context.vRouter.pathParameters['authorId']!)]
                                    .author,
                              );
                            }),
                          ),
                        ],
                      ),
                    ],
                  ),
                ],
              );
            },
          ),
        );
      }
    
      Future<void> authenticationCheck(BuildContext context, {required VRedirector vRedirector}) async {
        if (!Provider.of<AppState>(context, listen: false).isAuthenticated) {
          vRedirector.to('/login', queryParameters: {'redirectedFrom': '${vRedirector.toUrl}'});
        }
      }
    }
    
    class AuthenticationScreen extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Center(
          child: ElevatedButton(
            onPressed: () {
              Provider.of<AppState>(context, listen: false).authenticate();
              context.vRouter.to(context.vRouter.queryParameters['redirectedFrom'] == null
                  ? '/'
                  : context.vRouter.queryParameters['redirectedFrom']!);
            },
            child: Text('Click to login'),
          ),
        );
      }
    }
    
    class InfoWidget extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Text('Some info but actually there is nothing'),
        );
      }
    }
    
    
    class BooksListScreen extends StatelessWidget {
      final List<Book> books;
    
      BooksListScreen({required this.books});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(),
          body: ListView(
            children: [
              for (var book in books)
                ListTile(
                  title: Text(book.title),
                  subtitle: Text(book.author.name),
                  onTap: () => context.vRouter.to('/book/${books.indexOf(book)}'),
                )
            ],
          ),
        );
      }
    }
    
    class AuthorsListScreen extends StatelessWidget {
      final List<Author> authors;
    
      AuthorsListScreen({required this.authors});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(),
          body: ListView(
            children: [
              ElevatedButton(
                onPressed: () => context.vRouter.to('/'),
                child: Text('Go to Books Screen'),
              ),
              for (var author in authors)
                ListTile(
                  title: Text(author.name),
                  onTap: () => context.vRouter.to('/author/${authors.indexOf(author)}'),
                )
            ],
          ),
        );
      }
    }
    
    class BookDetailsScreen extends StatelessWidget {
      final Book book;
    
      BookDetailsScreen({required this.book});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(),
          body: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(book.title, style: Theme.of(context).textTheme.headline6),
                ElevatedButton(
                  onPressed: () {
                    context.vRouter.to('/author/${context.vRouter.pathParameters['bookId']}');
                  },
                  child: Text(book.author.name),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    class AuthorDetailsScreen extends StatelessWidget {
      final Author author;
    
      AuthorDetailsScreen({required this.author});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(),
          body: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(author.name, style: Theme.of(context).textTheme.headline6),
              ],
            ),
          ),
        );
      }
    }
    

    The trick is to use a VGuard which, before entering the stackedRoutes, checks whether or not the user is authenticated.

    I used queryParameters to store where the user it redirected from, however you could use historyState if you did not want where the user is redirected from appear in the url. That being said, I still prefer queryParameters in that case since is allows link sharing.