Closed affan3699 closed 3 weeks ago
Hi @affan3699 , make sure that SessionTimeoutManager
is the root widget of your widget tree, it shouldn't be rebuilt conditionally, otherwise its state will be lost leading to unexpected behavior. It looks like every time provider triggers a rebuild SessionTimeoutManager
widget will also be rebuilt which shouldn't happen in ideal scenario.
Its not working, even i have made it root widget, the Foreground timer is making issue here (invalidateSessionForAppLostFocus) . If you are saying bring the main.dart multiprovider under SessionTimeOut widget, then how will i access the provider or save the session state in provider
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
await DeviceDetails.instance.initialize();
if (Platform.isAndroid) {
AndroidGoogleMapsFlutter.useAndroidViewSurface = true;
}
await Future.delayed(const Duration(milliseconds: 150));
final sessionStateStream = StreamController<SessionState>();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<LoginProvider>(
create: (_) => LoginProvider(sessionStateStream)),
],
child: MyApp(sessionStateStream: sessionStateStream),
),
);
}
class MyApp extends StatelessWidget {
final sessionStateStream;
MyApp({super.key, this.sessionStateStream});
var logger = Logger(printer: PrettyPrinter());
final _navigatorKey = GlobalKey<NavigatorState>();
NavigatorState get _navigator => _navigatorKey.currentState!;
/// Make this stream available throughout the widget tree with with any state management library
/// like bloc, provider, GetX, ..
///
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
final loginProvider = Provider.of<LoginProvider>(context);
final sessionConfig = SessionConfig(
invalidateSessionForAppLostFocus: const Duration(minutes: 10),
invalidateSessionForUserInactivity: const Duration(minutes: 10));
sessionConfig.stream.listen((SessionTimeoutState timeoutEvent) {
// stop listening, as user will already be in auth page
loginProvider.sessionStateStream.add(SessionState.stopListening);
if (timeoutEvent == SessionTimeoutState.userInactivityTimeout) {
//MyToast.showToast('Session Timed Out');
logger.i("Logged out because of user inactivity");
Provider.of<LoginProvider>(context, listen: false).clear();
// handle user inactive timeout
ModalUtil.showErrorModal(
context: _navigatorKey.currentState!.overlay!.context,
title: 'Session Timed Out',
firstLineText: "Logged out because of user inactivity,\n",
secondLineText: 'dubara koshish karain!',
responseCode: '108',
);
// _navigator.pushAndRemoveUntil(
// MaterialPageRoute(
// builder: (context) => const WelcomeScreen(),
// ),
// (Route<dynamic> route) => false,
// );
} else if (timeoutEvent == SessionTimeoutState.appFocusTimeout) {
//MyToast.showToast('Session Timed Out');
logger.i("Logged out because of user inactivity ForeGround");
// handle user app lost focus timeout
ModalUtil.showErrorModal(
context: _navigatorKey.currentState!.overlay!.context,
title: 'Session Timed Out',
firstLineText: "Logged out because of user inactivity,\n",
secondLineText: 'dubara koshish karain!',
responseCode: '108',
);
}
});
OneSignal.Notifications.addForegroundWillDisplayListener((event) async {
/// Display Notification, preventDefault to not display
event.preventDefault();
/// Do async work
Provider.of<LoginProvider>(context, listen: false)
.updateShouldFetchTransactionListData();
print('Kaam hua');
/// notification.display() to display after preventing default
event.notification.display();
});
return SessionTimeoutManager(
sessionConfig: sessionConfig,
sessionStateStream: loginProvider.sessionStateStream.stream,
userActivityDebounceDuration: const Duration(seconds: 15),
child: ScreenUtilInit(
designSize: Size(390, 844),
minTextAdapt: true,
splitScreenMode: true,
// Use builder only if you need to use library outside ScreenUtilInit context
builder: (_, child) {
return MaterialApp(
navigatorKey: _navigatorKey,
debugShowCheckedModeBanner: false,
// You can use the library anywhere in the app even in theme
theme: ThemeData(
primarySwatch: Colors.blue,
cardColor: appTheme.blackLight,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
textTheme:
Typography.englishLike2018.apply(fontSizeFactor: 1.sp),
scaffoldBackgroundColor: Colors.white),
home: child,
routes: AppRoutes.routes,
);
},
child: WelcomeScreen(),
),
);
}
}
@affan3699 I have added dispose
to delete timers if the widget is removed. Can you reference this commit id 83695865167abc5b48f88e9ca15532443a534f5c in your code and check if it solves the issue?
No its not working. The foreground timer doesn't reset which is why it timeout. The UserInactivity is working perfectly fine. The appFocusTimeout is not working as expected.
@SankethBK Any solution for it? because my app will soon go Live.
@affan3699 if you can reproduce the same issue in the example app or any simple executable app, it will help me to find the cause
https://github.com/user-attachments/assets/52ca7ece-22c0-465a-a044-824eb383a48b
As you can see i have used the example app with latest flutter version. the config is: invalidateSessionForAppLostFocus: const Duration(minutes: 1), invalidateSessionForUserInactivity: const Duration(minutes: 5),
As you can see i have set invalidateSessionForAppLostFocus 1 minute, in the video above, my app only lost focus for 1 or 2 second. The timer does not reset and in the end when i again lost focus it times out. The example app has only 1 or 2 pages, but in real world app has many pages due to which even my actual app does not lost focus for even 1 time, it times out due to lost focus reason.
Note:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:local_session_timeout/local_session_timeout.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({super.key});
final _navigatorKey = GlobalKey<NavigatorState>();
NavigatorState get _navigator => _navigatorKey.currentState!;
/// Make this stream available throughout the widget tree with with any state management library
/// like bloc, provider, GetX, ..
final sessionStateStream = StreamController<SessionState>();
@override
Widget build(BuildContext context) {
final sessionConfig = SessionConfig(
invalidateSessionForAppLostFocus: const Duration(minutes: 1),
invalidateSessionForUserInactivity: const Duration(minutes: 5),
);
sessionConfig.stream.listen((SessionTimeoutState timeoutEvent) {
// stop listening, as user will already be in auth page
sessionStateStream.add(SessionState.stopListening);
if (timeoutEvent == SessionTimeoutState.userInactivityTimeout) {
print('logged out because of user inactivity');
// handle user inactive timeout
_navigator.push(MaterialPageRoute(
builder: (_) => AuthPage(
sessionStateStream: sessionStateStream,
loggedOutReason: "Logged out because of user inactivity"),
));
} else if (timeoutEvent == SessionTimeoutState.appFocusTimeout) {
print('Logged out because app lost focus');
// handle user app lost focus timeout
_navigator.push(MaterialPageRoute(
builder: (_) => AuthPage(
sessionStateStream: sessionStateStream,
loggedOutReason: "Logged out because app lost focus"),
));
}
});
return SessionTimeoutManager(
userActivityDebounceDuration: const Duration(seconds: 1),
sessionConfig: sessionConfig,
sessionStateStream: sessionStateStream.stream,
child: MaterialApp(
navigatorKey: _navigatorKey,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: AuthPage(
sessionStateStream: sessionStateStream,
),
),
);
}
}
// ignore: must_be_immutable
class AuthPage extends StatelessWidget {
AuthPage({
required this.sessionStateStream,
this.loggedOutReason = "",
super.key,
});
final StreamController<SessionState> sessionStateStream;
late String loggedOutReason;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (loggedOutReason != "")
Container(
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 15,
),
child: Text(loggedOutReason),
),
const SizedBox(
height: 20,
),
ElevatedButton(
onPressed: () async {
// start listening only after user logs in
sessionStateStream.add(SessionState.startListening);
loggedOutReason = await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => MyHomePage(
sessionStateStream: sessionStateStream,
),
),
);
},
child: const Text("Login"),
),
],
),
));
}
}
class MyHomePage extends StatelessWidget {
final StreamController<SessionState> sessionStateStream;
const MyHomePage({
required this.sessionStateStream,
super.key,
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("Home page"),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
},
child: const Text("Touch"),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () async {
// stop listening only after user goes to this page
//! Its better to handle listening logic in state management
//! libraries rather than writing them at random places in your app
sessionStateStream.add(SessionState.stopListening);
await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const ReadingPage(),
),
);
//! after user returns from that page start listening again
sessionStateStream.add(SessionState.startListening);
},
child: const Text("Reading page"),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => WritingPage(
sessionStream: sessionStateStream,
),
),
);
},
child: const Text("Writing Page"),
),
],
),
),
);
}
}
// This [age can text content, which user might be reading without any user activity
// If you want to disable the session timeout listeners when user enters such pages
// follow the below code
class ReadingPage extends StatefulWidget {
const ReadingPage({super.key});
@override
State<ReadingPage> createState() => _ReadingPageState();
}
class _ReadingPageState extends State<ReadingPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Text(
"This page can have lot of extent content, and user might be reading this without performing any user activity. So you might want to disable sesison timeout listeners only for this page"),
),
),
);
}
}
// If the user is typing into the textbox, you may want to disable the session
// timeout listeners because typing events aren't captured by session timeout
// manager and it may conclude that user is inactive
class WritingPage extends StatefulWidget {
final StreamController<SessionState> sessionStream;
const WritingPage({
required this.sessionStream,
super.key,
});
@override
State<WritingPage> createState() => _WritingPageState();
}
class _WritingPageState extends State<WritingPage> {
@override
Widget build(BuildContext context) {
if (MediaQuery.of(context).viewInsets.bottom > 0) {
// softkeyboard is open
widget.sessionStream.add(SessionState.stopListening);
} else {
// keyboard is closed
widget.sessionStream.add(SessionState.startListening);
}
return Scaffold(
appBar: AppBar(),
body: const Padding(
padding: EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"If the user is typing into the textbox, you may want to disable the session timeout listeners because typing events aren't captured by session timeout manager and it may conclude that user is inactive"),
SizedBox(
height: 20,
),
TextField(
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide:
BorderSide(color: Color.fromARGB(255, 16, 17, 17))),
hintText: 'Start typing here',
helperText: 'when keyboard is open, session won"t expire',
prefixText: ' ',
suffixText: 'USD',
suffixStyle: TextStyle(
color: Color.fromARGB(255, 14, 14, 14),
),
),
),
],
),
),
);
}
}
Hi @affan3699 , I got the issue. I have raised a fix in this commit 57023c451170904f7759cf7439d0b47a86d27120, can you try referencing this commit in your pubspec and verify.
Yes its ok now, i tested it. Thanks for your help. Will inform, if any other issue arise in future.
Once the application goes in background and resumed. the timer of appFocusTimeout i think didn't reset and it timeout after given specified time.