SankethBK / local_session_timeout

Redirect user to authentication page if the application doesn't receive any user interaction, or been running in the background for "x" duration.
https://pub.dev/packages/local_session_timeout
BSD 3-Clause "New" or "Revised" License
12 stars 19 forks source link

Flutter Web: SessionTimeoutState.userInactivityTimeout event keep calling even when user interacts with the app #24

Closed iKK001 closed 11 months ago

iKK001 commented 11 months ago

Using Flutter channel stable, 3.13.5 / Chrome - develop for the web :

I know people had asked about this before. But the solution provided did not help.

I am working on Flutter WEB where this library simply does not work for USER-INTERACTION timeoutEvents. The Web-Application delivers timeoutEvent's no matter how much I interact with my Flutter Web appliction.

(and I know that this library cannot deliver appFocusTimeout for Web - but I am talking about userInactivityTimeout that does not seem to work either for flutter Web.)

What does "userIneraction" mean exactly ?

I would like to prevent logout with this library for any of the following userInteractions:

Here is my code:

class MainPage extends StatelessWidget {
  const MainPage({super.key});

  @override
  Widget build(BuildContext context) {
    final sessionConfig = SessionConfig(
      invalidateSessionForAppLostFocus:
          const Duration(seconds: 20),
      invalidateSessionForUserInactivity:
          const Duration(seconds: 30),
    );

    sessionConfig.stream.listen((SessionTimeoutState timeoutEvent) {
      if (timeoutEvent == SessionTimeoutState.userInactivityTimeout) {
        // UNFORTUNATELY THE timeoutEvent KICKS IN ALWAYS - NO MATTER WHAT THE USER-INTERACTION IS !!!!!!!!!!!!
        Navigator.pushNamed(context, '/logout');
      } else if (timeoutEvent == SessionTimeoutState.appFocusTimeout) {
        Navigator.pushNamed(context, '/logout');
      }
    });
    return SessionTimeoutManager(
      sessionConfig: sessionConfig,
      userActivityDebounceDuration: const Duration(seconds: 1),
      child: MyApp(),
    );
  }
}

What is wrong still ?

iKK001 commented 11 months ago

I finally found a solution.

The reason for not-recognition of the userInteraction in my Flutter Web app is as follows:

I had the SessionTimeoutManager Widget too deep down the Widget tree ! And therefore the userInteractions where not recognized.

The reason I had it one level too deep down was the following:

If you wrap the SessionTimeoutManager arround MaterialApp (as stated in your example on the main page of your github repo), then you will get the error (at the timeoutEvent callback location) that the context cannot use Navigator ! Therefore if one wants to listen to userInteraction-timeoutEvents (and doing a Navigator.pushNamed(...) on it), then you must hand the sessionConfig as a parameter down the widget-tree to the location where the context can fulfill a Navigator action !

See the working example here !

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  final sessionConfig = SessionConfig(
    invalidateSessionForAppLostFocus:
        const Duration(seconds: constants.AUTO_LOGOUT_TIMER_DURATION),
    invalidateSessionForUserInactivity:
        const Duration(seconds: constants.AUTO_LOGOUT_TIMER_DURATION),
  );

  @override
  Widget build(BuildContext context) {
    return SessionTimeoutManager(
      sessionConfig: sessionConfig,
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: const MainPage(sessionConfig: sessionConfig), // GIVE IT AS A PARAMETER DOWN THE WIDGET-TREE HERE...
        routes: {
            '/': (context) => const FirstScreen(),
            '/auth': (context) => const AuthScreen(),
          },
      ),
    );
  }
}

class MainPage extends StatelessWidget {
  final SessionConfig sessionConfig;

  const MainPage({
    super.key,
    required this.sessionConfig,
  });

  @override
  Widget build(BuildContext context) {
    sessionConfig.stream.listen((SessionTimeoutState timeoutEvent) {
      if (timeoutEvent == SessionTimeoutState.userInactivityTimeout) {
        Navigator.pushNamed(context, '/logout');
      } else if (timeoutEvent == SessionTimeoutState.appFocusTimeout) {
        Navigator.pushNamed(context, '/logout');
      }
    });
    return MyApp();
  }
}

I find the example code shown in your main github and dart-package site very much misleading.

As a suggestion, you should explain that Navigator.of(context).pushNamed("/auth"); will not work with your example shown. And that you need to send down the sessionConfig down the widget-tree to an appropriate location where the context will allow Naviagor to work.

Moreover, it is important that SessionTimeoutManager is placed above MaterialApp if routing is used.

iKK001 commented 11 months ago

And an even better solution is to use the following :

final navigatorKey = GlobalKey<NavigatorState>();
NavigatorState get _navigator => navigatorKey.currentState!;

This allows to make navigation without the context (i.e. when SessionTimeoutManager is wrapping MaterialApp then you cannot use context for Navigator.pushNamed(...). Therefore the NavigatorState mechano !

    sessionConfig.stream.listen((SessionTimeoutState timeoutEvent) {
      if (timeoutEvent == SessionTimeoutState.userInactivityTimeout) {
        _navigator.pushNamed('/logout');
      } else if (timeoutEvent == SessionTimeoutState.appFocusTimeout) {
        _navigator.pushNamed('/logout');
      }
    });
SankethBK commented 11 months ago

Thanks for your input. I will update the readme accordingly