flutterauthenticationflutter-blocflutter-go-router

Authenticate with Go Router and BLoC in flutter


I am using Go Router together with BLoC in a web app to manage the user session. What I am looking for is that, at the moment of entering a certain route, to be able to validate if a user is authenticated. Inside the BLoC I have a function that validates if a token exists (it returns “false” or “true”). My problem is that, in the case of refreshing the page, the path logic is executed first and then the BLoC logic is executed. Then, if there is a user already logged in, the path guard will take the initial state of “isAuth” which is false, but then if it will be true, since a token does exist. What can I do?

Router:

final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();

class AppRouter {
  final GoRouter router = GoRouter(
    initialLocation: Uri.base.toString().replaceFirst(Uri.base.origin, ''),
    navigatorKey: _navigatorKey,
    routes: <RouteBase>[
      GoRoute(
        path: Routes.home,
        builder: (BuildContext context, GoRouterState state) =>
            BlocProvider<SignInCubit>.value(
          value: locator<SignInCubit>(),
          child: const SignInScreen(),
        ),
      ),
      GoRoute(
        path: Routes.paymentConfirmation,
        builder: (BuildContext context, GoRouterState state) =>
            BlocProvider<PaymentConfirmationCubit>(
          create: (_) => locator<PaymentConfirmationCubit>(),
          child: const PaymentConfirmation(),
        ),
        redirect: (BuildContext context, _) {
          if (context.read<SessionBloc>().state.isAuth) {
            return null;
          }
          return Routes.home;
        },
      ),
      GoRoute(
        path: Routes.otpCodeValidation,
        builder: (BuildContext context, GoRouterState state) =>
            BlocProvider<OtpCodeCubit>.value(
          value: locator<OtpCodeCubit>(),
          child: const OtpCodeValidation(),
        ),
      ),
      GoRoute(
        path: Routes.paymentValidation,
        builder: (BuildContext context, GoRouterState state) =>
            const PaymentValidation(),
      ),
      GoRoute(
        path: Routes.transactionsList,
        builder: (BuildContext context, GoRouterState state) =>
            const TransactionsList(),
      ),
      GoRoute(
        path: Routes.transactionDetail,
        builder: (BuildContext context, GoRouterState state) =>
            const TransactionDetail(),
      ),
    ],
  );
}

BLoC:

class SessionBloc extends Bloc<SessionEvent, SessionState> {
  SessionBloc({
    required this.validateTokenUseCase,
    required this.signInUseCase,
  }) : super(SessionState(user: User.empty())) {
    on<SessionStarted>(_onSessionStarted);
  }

  final ValidateTokenUseCase validateTokenUseCase;
  final SignInUseCase signInUseCase;

  Future<void> _onSessionStarted(
      SessionStarted event, Emitter<SessionState> emit) async {
    emit(state.copyWith(isLoading: true));
    try {
      final bool isValid = await validateTokenUseCase.call();
      emit(state.copyWith(isLoading: false, isAuth: isValid));
      // emit(state.copyWith(user: event.user));
    } catch (e) {
      emit(state.copyWith(isLoading: false));
      rethrow;
    }
  }
}

App:

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return BlocProvider<SessionBloc>(
      create: (_) => locator<SessionBloc>()..add(const SessionStarted()),
      child: OKToast(
        child: MaterialApp.router(
          builder: (BuildContext context, Widget? widget) {
            return OKToast(child: widget!);
          },
          debugShowCheckedModeBanner: false,
          theme: appTheme,
          routerConfig: AppRouter().router,
          localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
            GlobalMaterialLocalizations.delegate,
            GlobalCupertinoLocalizations.delegate,
            GlobalWidgetsLocalizations.delegate
          ],
          supportedLocales: LocaleSettings.instance.supportedLocales,
          locale: TranslationProvider.of(context).flutterLocale,
          title: 'Orquestador Pideky Escala',
        ),
      ),
    );
  }
}

Solution

  • I managed to solve my problem by wrapping my “MaterialApp.router” in a BlocListener, and refreshing the router (in listener directive) when AuthState changes. Like this:

    return MultiBlocProvider(
      providers: <SingleChildWidget>[
        BlocProvider<AuthBloc>(
          create: (_) => locator<AuthBloc>()..add(const VerifyAuth()),
        ),
        BlocProvider<SessionBloc>(
          create: (_) => locator<SessionBloc>()..add(const SessionStarted()),
        ),
      ],
      child: BlocListener<AuthBloc, AuthState>(
          listener: (BuildContext context, AuthState state) {
            router.refresh();
          },
          child: MaterialApp.router(
            builder: (BuildContext context, Widget? widget) {
              return OKToast(child: widget!);
            },
            debugShowCheckedModeBanner: false,
            theme: appTheme,
    
    ...rest of the code
    

    Obviously, a Provider (in my case a MultiBlocProvider) is needed to listen the changes.