diff --git a/lib/src/widgets/solid_login.dart b/lib/src/widgets/solid_login.dart index c13ab3d..f39496b 100644 --- a/lib/src/widgets/solid_login.dart +++ b/lib/src/widgets/solid_login.dart @@ -208,6 +208,10 @@ class _SolidLoginState extends State with WidgetsBindingObserver { _infoFocusNode = FocusNode(debugLabel: 'infoButton'); _serverInputFocusNode = FocusNode(debugLabel: 'serverInput'); + // Restore the persisted "Stay signed in" preference. + + _loadStaySignedInPreference(); + // Resolve image assets with fallback logic. _resolveImageAssets(); @@ -253,6 +257,13 @@ class _SolidLoginState extends State with WidgetsBindingObserver { if (mounted) setState(() => _assetsResolved = true); } + /// Loads the persisted "Stay signed in" preference from SharedPreferences. + + Future _loadStaySignedInPreference() async { + final value = await SolidLoginAuthHandler.getStaySignedIn(); + if (mounted) setState(() => _staySignedIn = value); + } + @override void dispose() { WidgetsBinding.instance.removeObserver(this); @@ -370,6 +381,14 @@ class _SolidLoginState extends State with WidgetsBindingObserver { // - No cache → start the browser login flow as normal. Future performLogin() async { + // When the user has opted out of staying signed in, discard any + // existing cached session immediately so browser authentication + // is always required. + + if (!_staySignedIn) { + await deleteLogIn(); + } + final podServer = webIdController.text.trim().isNotEmpty ? webIdController.text.trim() : SolidConfig.defaultServerUrl; @@ -415,6 +434,14 @@ class _SolidLoginState extends State with WidgetsBindingObserver { // re-login so the setup wizard can re-initialise the POD. Future performContinue() async { + // When the user has opted out of staying signed in, discard any + // existing cached session immediately so the user proceeds in a + // logged-out state. + + if (!_staySignedIn) { + await deleteLogIn(); + } + final isLoggedIn = await isUserLoggedIn(); if (isLoggedIn && defaultFolders.isNotEmpty) { @@ -529,14 +556,21 @@ class _SolidLoginState extends State with WidgetsBindingObserver { height: 24, child: Checkbox( value: _staySignedIn, - onChanged: (value) => - setState(() => _staySignedIn = value ?? true), + onChanged: (value) { + final newValue = value ?? true; + setState(() => _staySignedIn = newValue); + SolidLoginAuthHandler.setStaySignedIn(newValue); + }, ), ), ), const SizedBox(width: 8), GestureDetector( - onTap: () => setState(() => _staySignedIn = !_staySignedIn), + onTap: () { + final newValue = !_staySignedIn; + setState(() => _staySignedIn = newValue); + SolidLoginAuthHandler.setStaySignedIn(newValue); + }, child: Text( 'Stay signed in', style: TextStyle(color: currentTheme.textColor, fontSize: 14), diff --git a/lib/src/widgets/solid_login_auth_handler.dart b/lib/src/widgets/solid_login_auth_handler.dart index 14194eb..341e10c 100644 --- a/lib/src/widgets/solid_login_auth_handler.dart +++ b/lib/src/widgets/solid_login_auth_handler.dart @@ -56,6 +56,10 @@ class SolidLoginAuthHandler { static const clearSessionKey = 'solidui_clear_session_on_startup'; + /// SharedPreferences key for persisting the "Stay signed in" preference. + + static const staySignedInKey = 'solidui_stay_signed_in'; + /// Clears the cached login session if a previous session opted out of /// "Stay signed in". Call this early during login page initialisation. @@ -68,6 +72,21 @@ class SolidLoginAuthHandler { } } + /// Returns the persisted "Stay signed in" preference, defaulting to true. + + static Future getStaySignedIn() async { + final prefs = await SharedPreferences.getInstance(); + + return prefs.getBool(staySignedInKey) ?? true; + } + + /// Persists the "Stay signed in" preference. + + static Future setStaySignedIn(bool value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(staySignedInKey, value); + } + /// Notifies the user that their POD is not initialised, verifies the remote /// directory structure, and navigates to the appropriate screen (setup wizard /// or child widget). @@ -97,6 +116,16 @@ class SolidLoginAuthHandler { if (!allExists) { await clearPodStructureInitialised(); + + // Schedule session clearance for next startup when the user has + // opted out of staying signed in. We must not call deleteLogIn() + // here because InitialSetupScreen still needs valid auth data. + + if (!staySignedIn) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(clearSessionKey, true); + } + if (!context.mounted) return false; await pushReplacement( @@ -109,15 +138,20 @@ class SolidLoginAuthHandler { ); } else { await markPodStructureInitialised(); + + // Schedule session clearance for next startup when the user has + // opted out of staying signed in. deleteLogIn() must come after + // markPodStructureInitialised() which requires valid auth data. + + if (!staySignedIn) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(clearSessionKey, true); + } + if (!context.mounted) return false; await pushReplacement(context, childWidget); } - if (!staySignedIn) { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(clearSessionKey, true); - } - return true; } @@ -285,6 +319,15 @@ class SolidLoginAuthHandler { await clearPodStructureInitialised(); + // Schedule session clearance for next startup when the user has + // opted out of staying signed in. We must not call deleteLogIn() + // here because InitialSetupScreen still needs valid auth data. + + if (!staySignedIn) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(clearSessionKey, true); + } + if (!context.mounted) return false; await pushReplacement( @@ -297,15 +340,20 @@ class SolidLoginAuthHandler { ); } else { await markPodStructureInitialised(); + + // Schedule session clearance for next startup when the user has + // opted out of staying signed in. deleteLogIn() must come after + // markPodStructureInitialised() which requires valid auth data. + + if (!staySignedIn) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(clearSessionKey, true); + } + if (!context.mounted) return false; await pushReplacement(context, childWidget); } - if (!staySignedIn) { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool(clearSessionKey, true); - } - return true; } else { // solidAuthenticate() returned null. This can happen when: diff --git a/lib/src/widgets/solid_login_panel.dart b/lib/src/widgets/solid_login_panel.dart index 0198802..397c060 100644 --- a/lib/src/widgets/solid_login_panel.dart +++ b/lib/src/widgets/solid_login_panel.dart @@ -124,11 +124,21 @@ class SolidLoginPanel { ], ), - if (staySignedInCheckbox != null) staySignedInCheckbox, - if (tryAnotherAccountButton != null) ...[ - const SizedBox(height: 4.0), + if (staySignedInCheckbox != null) ...[ + if (tryAnotherAccountButton != null) + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + staySignedInCheckbox, + const SizedBox(width: 16.0), + tryAnotherAccountButton, + ], + ) + else + staySignedInCheckbox, + ] else if (tryAnotherAccountButton != null) tryAnotherAccountButton, - ], const SizedBox(height: 20.0), diff --git a/lib/src/widgets/solid_scaffold_state.dart b/lib/src/widgets/solid_scaffold_state.dart index c69323a..1203dd2 100644 --- a/lib/src/widgets/solid_scaffold_state.dart +++ b/lib/src/widgets/solid_scaffold_state.dart @@ -114,7 +114,16 @@ class SolidScaffoldState extends State { super.dispose(); } - void _onPreferencesChanged() => mounted ? setState(() {}) : null; + // Deferred to avoid triggering setState while the framework is building + // widgets (e.g. when initializeIfNeeded updates the notifier during build). + + void _onPreferencesChanged() { + if (!mounted) return; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) setState(() {}); + }); + } + void _onControllerChanged() => mounted ? setState(() {}) : null; void _onThemeChanged() => mounted ? setState(() {}) : null;