From 65de64e0ccb83b69197cd1f8498d10c10eef4011 Mon Sep 17 00:00:00 2001 From: Graham Williams Date: Fri, 2 Oct 2026 20:31:47 +1000 Subject: [PATCH] Bump version 1.4.0 Apps can skip the login page when Continue is on SolidLogin takes skipLogin (default false). When CONTINUE is offered and skipping is on, start-up goes straight into the app as though CONTINUE were tapped, once per run, so logout and in-app login still show the login page. The Settings dialogue gains a Login Page section for the user to override the app's choice. Closes #423. --- CHANGELOG.md | 4 + lib/src/utils/solid_skip_login.dart | 119 ++++++++++++ lib/src/widgets/solid_login.dart | 103 +++++----- lib/src/widgets/solid_login_auto_config.dart | 58 ++++++ lib/src/widgets/solid_preferences_dialog.dart | 31 ++- .../widgets/solid_settings_login_section.dart | 88 +++++++++ pubspec.yaml | 2 +- test/solid_skip_login_test.dart | 179 ++++++++++++++++++ 8 files changed, 531 insertions(+), 53 deletions(-) create mode 100644 lib/src/utils/solid_skip_login.dart create mode 100644 lib/src/widgets/solid_login_auto_config.dart create mode 100644 lib/src/widgets/solid_settings_login_section.dart create mode 100644 test/solid_skip_login_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 8faccc38..552594cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ The package is available from [//]: # (au](https://solidcommunity.au/docs/solidui)) +## 1.4 Skip Login + ++ Apps can skip the login page when Continue is on [1.4.0 20261002 gjw] + ## 1.3 + Login no longer asks for the security key twice [1.3.1 20261002 gjw] diff --git a/lib/src/utils/solid_skip_login.dart b/lib/src/utils/solid_skip_login.dart new file mode 100644 index 00000000..7b867a4a --- /dev/null +++ b/lib/src/utils/solid_skip_login.dart @@ -0,0 +1,119 @@ +/// Whether the app goes straight in at start-up, as if CONTINUE were tapped. +/// +/// Copyright (C) 2026, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Graham Williams + +library; + +import 'package:flutter/foundation.dart'; + +import 'package:shared_preferences/shared_preferences.dart'; + +/// Skip the login page at start-up, going straight into the app exactly as +/// though the user had tapped CONTINUE. +/// +/// Only an app that offers CONTINUE (a `SolidLogin` with `required: false`) +/// can skip. The app chooses the default through `SolidLogin.skipLogin`, and +/// the user can override it from the Login Page section of the settings +/// dialogue. The choice is a device preference, kept in SharedPreferences. +/// +/// The decision is made once, by the first `SolidLogin` of the run. The login +/// page an app returns to on logout, or opens to log in from within the app, +/// is also a `SolidLogin`, and skipping that would make logging in from the +/// app impossible. + +class SolidSkipLogin { + SolidSkipLogin._(); + + /// The preference the user's own choice is kept in. It is absent while the + /// user follows the app's default. + + static const String skipPref = 'solidui.skipLogin'; + + static bool _decided = false; + + static bool? _appDefault; + + static bool _skipping = false; + + /// Whether the app offers CONTINUE, and so whether there is anything for + /// the settings dialogue to offer. + + static bool get offered => _appDefault != null; + + /// The app's own default, so the settings dialogue can restore it. + + static bool get appDefault => _appDefault ?? false; + + /// Whether the login page is currently skipped at start-up. + + static bool get skipping => _skipping; + + /// Decide, once per run, whether to skip the login page. + /// + /// Returns true only on the first call, and only when the app offers + /// CONTINUE ([required] is false) and skipping is the user's choice or, + /// failing that, the app's default [byDefault]. + + static Future atStartup({ + required bool required, + required bool byDefault, + }) async { + if (_decided) return false; + _decided = true; + if (required) return false; + + _appDefault = byDefault; + final prefs = await SharedPreferences.getInstance(); + _skipping = prefs.getBool(skipPref) ?? byDefault; + + return _skipping; + } + + /// Record whether to skip from the next start-up on. A choice that matches + /// the app's default is forgotten, so the user follows the app's default + /// again should the app change it. + + static Future setSkipping(bool value) async { + _skipping = value; + final prefs = await SharedPreferences.getInstance(); + + if (value == _appDefault) { + await prefs.remove(skipPref); + } else { + await prefs.setBool(skipPref, value); + } + } + + /// Forget the decision for this run, so a test can start afresh. + + @visibleForTesting + static void reset() { + _decided = false; + _appDefault = null; + _skipping = false; + } +} diff --git a/lib/src/widgets/solid_login.dart b/lib/src/widgets/solid_login.dart index e7472395..61b699a0 100644 --- a/lib/src/widgets/solid_login.dart +++ b/lib/src/widgets/solid_login.dart @@ -47,11 +47,12 @@ import 'package:solidpod/solidpod.dart' tryRestoreSession; import 'package:solidui/src/constants/solid_config.dart'; -import 'package:solidui/src/handlers/solid_auth_handler.dart'; import 'package:solidui/src/models/snackbar_config.dart'; +import 'package:solidui/src/utils/solid_skip_login.dart'; import 'package:solidui/src/widgets/solid_login_actions.dart'; import 'package:solidui/src/widgets/solid_login_asset_helper.dart'; import 'package:solidui/src/widgets/solid_login_auth_handler.dart'; +import 'package:solidui/src/widgets/solid_login_auto_config.dart'; import 'package:solidui/src/widgets/solid_login_build_helper.dart'; import 'package:solidui/src/widgets/solid_login_helper.dart'; import 'package:solidui/src/widgets/solid_login_panel.dart'; @@ -89,6 +90,7 @@ class SolidLogin extends StatefulWidget { required this.redirectUris, this.postLogoutRedirectUris = const [], this.autoLogin = true, + this.skipLogin = false, super.key, }); @@ -189,6 +191,15 @@ class SolidLogin extends StatefulWidget { final bool autoLogin; + /// When true, and CONTINUE is offered ([required] is false), the app starts + /// as though the user had tapped CONTINUE, without showing the login page. + /// + /// This is the app's default, off unless the app asks for it. Either way the + /// user can change it from the settings dialogue. Only the app's start-up is skipped: the login + /// page shown on logout, or to log in from within the app, always appears. + + final bool skipLogin; + @override State createState() => _SolidLoginState(); } @@ -233,6 +244,10 @@ class _SolidLoginState extends State with WidgetsBindingObserver { bool _checkingAutoLogin = false; + /// Whether the login page is being skipped (shows a loading screen). + + bool _skipping = false; + /// Whether the user wishes to persist the login session across app restarts. bool _staySignedIn = true; @@ -266,12 +281,11 @@ class _SolidLoginState extends State with WidgetsBindingObserver { SolidLoginAuthHandler.clearSessionIfRequired(); - // If autoLogin is requested, attempt silent session restoration after the - // first frame so that context is available for navigation. + // If autoLogin is requested, attempt silent session restoration, and skip + // the login page if asked to, after the first frame so that context is + // available for navigation. - if (widget.autoLogin) { - WidgetsBinding.instance.addPostFrameCallback((_) => _checkAutoLogin()); - } + WidgetsBinding.instance.addPostFrameCallback((_) => _startUp()); // dc 20251022: please explain why calling an async without await. @@ -299,26 +313,22 @@ class _SolidLoginState extends State with WidgetsBindingObserver { // including button styles and theme so the login page appearance is // preserved across logout/re-login cycles. - SolidAuthHandler.instance.autoConfigureFromLogin( - title: widget.title, - appDirectory: widget.appDirectory, - webId: widget.webID, - image: widget.image, - logo: widget.logo, - link: widget.link, - child: widget.child, - loginButtonStyle: widget.loginButtonStyle, - continueButtonStyle: widget.continueButtonStyle, - registerButtonStyle: widget.registerButtonStyle, - infoButtonStyle: widget.infoButtonStyle, - changeKeyButtonStyle: widget.changeKeyButtonStyle, - themeConfig: widget.themeConfig, - snackbarConfig: widget.snackbarConfig, + autoConfigureAuthHandler(widget); + } + + /// Restore a saved session if asked to, and otherwise, when the login page + /// is to be skipped, carry on into the app just as CONTINUE does. + + Future _startUp() async { + final skip = await SolidSkipLogin.atStartup( required: widget.required, - clientId: widget.clientId, - redirectUris: widget.redirectUris, - postLogoutRedirectUris: widget.postLogoutRedirectUris, + byDefault: widget.skipLogin, ); + if (skip && mounted) setState(() => _skipping = true); + + final restored = widget.autoLogin && await _checkAutoLogin(); + if (skip && !restored && mounted) await _performContinue(); + if (mounted) setState(() => _skipping = false); } /// Resolves the image and logo assets with fallback logic. @@ -348,14 +358,15 @@ class _SolidLoginState extends State with WidgetsBindingObserver { } /// Attempts silent session restoration. On success navigates directly to - /// [widget.child]; on failure shows the login page as normal. + /// [widget.child]; on failure shows the login page as normal. Returns + /// whether a session was restored. - Future _checkAutoLogin() async { - if (!mounted) return; + Future _checkAutoLogin() async { + if (!mounted) return false; // Honour the "Stay signed in" opt-out — if disabled, skip auto-login. final staySignedIn = await SolidLoginAuthHandler.getStaySignedIn(); - if (!staySignedIn || !mounted) return; + if (!staySignedIn || !mounted) return false; if (mounted) setState(() => _checkingAutoLogin = true); @@ -387,13 +398,13 @@ class _SolidLoginState extends State with WidgetsBindingObserver { debugPrint('_checkAutoLogin: tryRestoreSession failed: $e'); if (mounted) setState(() => _checkingAutoLogin = false); - return; + return false; } - if (!mounted) return; + if (!mounted) return false; if (session == null) { setState(() => _checkingAutoLogin = false); - return; + return false; } // 20260728 gjw Session restored. Previously we navigated straight to @@ -414,10 +425,19 @@ class _SolidLoginState extends State with WidgetsBindingObserver { if (defaultFolders.isEmpty) await _loadDefaultStructure(); - if (!mounted) return; + if (!mounted) return false; setState(() => _checkingAutoLogin = false); + await _performContinue(); + + return true; + } + + /// The CONTINUE action, shared by the button, a restored session, and a + /// skipped login page. - await SolidLoginActions.performContinue( + Future _performContinue() { + _resetDialogCanceledState(); + return SolidLoginActions.performContinue( context: context, childWidget: widget.child, defaultFolders: defaultFolders, @@ -543,7 +563,7 @@ class _SolidLoginState extends State with WidgetsBindingObserver { // Show a loading indicator whilst assets are being resolved or an // auto-login check is in progress. - if (!_assetsResolved || _checkingAutoLogin) { + if (!_assetsResolved || _checkingAutoLogin || _skipping) { return const Scaffold(body: Center(child: CircularProgressIndicator())); } @@ -583,19 +603,6 @@ class _SolidLoginState extends State with WidgetsBindingObserver { ); } - Future performContinue() { - _resetDialogCanceledState(); - return SolidLoginActions.performContinue( - context: context, - childWidget: widget.child, - defaultFolders: defaultFolders, - defaultFiles: defaultFiles, - updateDialogCanceledState: updateState, - showSnackbar: _showSnackbar, - staySignedIn: _staySignedIn, - ); - } - Future performTryAnotherAccount() => SolidLoginActions.performTryAnotherAccount( context: context, @@ -620,7 +627,7 @@ class _SolidLoginState extends State with WidgetsBindingObserver { final continueButton = SolidLoginBuildHelper.buildContinueButton( context: context, style: widget.continueButtonStyle, - performContinue: performContinue, + performContinue: _performContinue, focusNode: _continueFocusNode, ); diff --git a/lib/src/widgets/solid_login_auto_config.dart b/lib/src/widgets/solid_login_auto_config.dart new file mode 100644 index 00000000..403116a9 --- /dev/null +++ b/lib/src/widgets/solid_login_auto_config.dart @@ -0,0 +1,58 @@ +/// Configure the auth handler from the app's SolidLogin. +/// +/// Copyright (C) 2026, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Graham Williams + +library; + +import 'package:solidui/src/handlers/solid_auth_handler.dart'; +import 'package:solidui/src/widgets/solid_login.dart'; + +/// Hand [login]'s settings to [SolidAuthHandler], so that a re-login from +/// within the app uses the same configuration, button styles and theme. + +void autoConfigureAuthHandler(SolidLogin login) { + SolidAuthHandler.instance.autoConfigureFromLogin( + title: login.title, + appDirectory: login.appDirectory, + webId: login.webID, + image: login.image, + logo: login.logo, + link: login.link, + child: login.child, + loginButtonStyle: login.loginButtonStyle, + continueButtonStyle: login.continueButtonStyle, + registerButtonStyle: login.registerButtonStyle, + infoButtonStyle: login.infoButtonStyle, + changeKeyButtonStyle: login.changeKeyButtonStyle, + themeConfig: login.themeConfig, + snackbarConfig: login.snackbarConfig, + required: login.required, + clientId: login.clientId, + redirectUris: login.redirectUris, + postLogoutRedirectUris: login.postLogoutRedirectUris, + ); +} diff --git a/lib/src/widgets/solid_preferences_dialog.dart b/lib/src/widgets/solid_preferences_dialog.dart index 619bc654..bdab0bde 100644 --- a/lib/src/widgets/solid_preferences_dialog.dart +++ b/lib/src/widgets/solid_preferences_dialog.dart @@ -31,20 +31,22 @@ library; import 'package:flutter/material.dart'; import 'package:solidui/src/utils/is_desktop.dart'; +import 'package:solidui/src/utils/solid_skip_login.dart'; import 'package:solidui/src/widgets/solid_preferences_appbar_defaults.dart'; import 'package:solidui/src/widgets/solid_preferences_button_order.dart'; import 'package:solidui/src/widgets/solid_preferences_models.dart'; import 'package:solidui/src/widgets/solid_preferences_notifier.dart'; +import 'package:solidui/src/widgets/solid_settings_login_section.dart'; import 'package:solidui/src/widgets/solid_settings_menu_section.dart'; import 'package:solidui/src/widgets/solid_settings_window_size_section.dart'; /// A dialogue of user settings, one section for each group of them: the -/// AppBar button order, where the menu sits on a narrow screen, and the size -/// of the desktop window. +/// AppBar button order, where the menu sits on a narrow screen, the size of +/// the desktop window, and whether the login page is shown at start-up. /// /// A section appears only where it applies, so an app that has turned off the -/// menu preferences, or one running on the web where there is no window to -/// size, simply shows fewer sections. Adding a section here adds it to every +/// menu preferences, one running on the web where there is no window to +/// size, or one that requires a login, simply shows fewer sections. Adding a section here adds it to every /// solidui app. class SolidPreferencesDialog extends StatefulWidget { @@ -105,6 +107,7 @@ class SolidPreferencesDialog extends StatefulWidget { class _SolidPreferencesDialogState extends State { late List _appBarActions; late bool _menuInBottomBar; + bool _showLoginPage = !SolidSkipLogin.skipping; // The window size section loads and applies its own values, so the dialogue // reaches it through its state rather than holding them here. @@ -116,6 +119,10 @@ class _SolidPreferencesDialogState extends State { bool get _showWindowSection => isDesktop; + /// Only an app that offers CONTINUE has a login page to skip. + + bool get _showLoginSection => SolidSkipLogin.offered; + @override void initState() { super.initState(); @@ -172,6 +179,8 @@ class _SolidPreferencesDialogState extends State { if (!saved) return; } + if (_showLoginSection) await SolidSkipLogin.setSkipping(!_showLoginPage); + final newConfig = SolidPreferencesConfig(appBarActions: _appBarActions); solidPreferencesNotifier.setConfig(newConfig); @@ -188,6 +197,7 @@ class _SolidPreferencesDialogState extends State { setState(() { _appBarActions = solidDefaultAppBarActions(_appBarActions); _menuInBottomBar = widget.scaffoldMenuInBottomBar; + _showLoginPage = !SolidSkipLogin.appDefault; }); _windowSize.currentState?.restoreDefault(); @@ -240,6 +250,19 @@ class _SolidPreferencesDialogState extends State { const SizedBox(height: 8), SolidSettingsWindowSizeSection(key: _windowSize), ], + if (_showLoginSection) ...[ + _sectionDivider( + widget.showAppBarSection || + widget.showMenuSection || + _showWindowSection, + ), + _sectionHeader(theme, 'Login Page'), + const SizedBox(height: 8), + SolidSettingsLoginSection( + showLoginPage: _showLoginPage, + onChanged: (value) => setState(() => _showLoginPage = value), + ), + ], ], ), ), diff --git a/lib/src/widgets/solid_settings_login_section.dart b/lib/src/widgets/solid_settings_login_section.dart new file mode 100644 index 00000000..4ed6c4d3 --- /dev/null +++ b/lib/src/widgets/solid_settings_login_section.dart @@ -0,0 +1,88 @@ +/// The Login Page section of the settings dialogue. +/// +/// Copyright (C) 2026, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Graham Williams + +library; + +import 'package:flutter/material.dart'; + +import 'package:markdown_tooltip/markdown_tooltip.dart'; + +/// Choose whether the login page is shown when the app starts, or the app +/// goes straight in as though CONTINUE had been tapped. +/// +/// The value is held by the enclosing dialogue, which saves it through +/// `SolidSkipLogin`. The dialogue shows this section only for an app that +/// offers CONTINUE. + +class SolidSettingsLoginSection extends StatelessWidget { + /// Whether the login page is shown at start-up. + + final bool showLoginPage; + + /// Called as the user flips the switch. + + final ValueChanged onChanged; + + const SolidSettingsLoginSection({ + super.key, + required this.showLoginPage, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'The app works without a Solid Pod. You can log in to your Pod ' + 'at any time from within the app.', + ), + const SizedBox(height: 16), + MarkdownTooltip( + message: ''' + + **Show the login page** + + When on, the app opens on its login page, from where you can log + in to your Solid Pod or continue without one. When off, the app + opens straight into its own pages, as though you had chosen to + continue. You are still logged in automatically if you chose to + stay signed in. + + ''', + child: SwitchListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Show the login page on start-up'), + value: showLoginPage, + onChanged: onChanged, + ), + ), + ], + ); +} diff --git a/pubspec.yaml b/pubspec.yaml index 74ed5db4..06ce8c2b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: solidui description: 'A UI library for building Solid applications with Flutter.' -version: 1.3.1 +version: 1.4.0 homepage: https://github.com/anusii/solidui # Scaffold a new Solid Pod file-browser app (a pod browser, with navigation diff --git a/test/solid_skip_login_test.dart b/test/solid_skip_login_test.dart new file mode 100644 index 00000000..8b12c2b2 --- /dev/null +++ b/test/solid_skip_login_test.dart @@ -0,0 +1,179 @@ +/// Tests for skipping the login page at start-up. +/// +/// Copyright (C) 2026, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Graham Williams + +library; + +import 'package:flutter/material.dart'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:solidui/src/utils/solid_skip_login.dart'; +import 'package:solidui/src/widgets/solid_preferences_dialog.dart'; +import 'package:solidui/src/widgets/solid_settings_login_section.dart'; + +import 'solid_window_size_test.dart' show mockWindow, mockWindowManager; + +/// Pump the settings dialogue on its own, without the AppBar section, which +/// belongs to a running scaffold. + +Future _pump(WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: SolidPreferencesDialog(showAppBarSection: false)), + ), + ); + await tester.pumpAndSettle(); +} + +/// The value of the Show the login page switch. + +bool _shown(WidgetTester tester) => tester + .widget( + find.widgetWithText(SwitchListTile, 'Show the login page on start-up'), + ) + .value; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + SharedPreferences.setMockInitialValues({}); + SolidSkipLogin.reset(); + mockWindow = const Size(1000, 700); + mockWindowManager(); + }); + + group('SolidSkipLogin.atStartup', () { + test('skips by the app default when CONTINUE is offered', () async { + expect( + await SolidSkipLogin.atStartup(required: false, byDefault: true), + isTrue, + ); + expect(SolidSkipLogin.offered, isTrue); + }); + + test('shows the login page when the app default says so', () async { + expect( + await SolidSkipLogin.atStartup(required: false, byDefault: false), + isFalse, + ); + expect(SolidSkipLogin.offered, isTrue); + }); + + test('never skips, nor offers to, when a login is required', () async { + expect( + await SolidSkipLogin.atStartup(required: true, byDefault: true), + isFalse, + ); + expect(SolidSkipLogin.offered, isFalse); + }); + + test('the user choice overrides the app default', () async { + SharedPreferences.setMockInitialValues({SolidSkipLogin.skipPref: false}); + + expect( + await SolidSkipLogin.atStartup(required: false, byDefault: true), + isFalse, + ); + }); + + test('decides only once, so a re-login page is never skipped', () async { + await SolidSkipLogin.atStartup(required: false, byDefault: true); + + expect( + await SolidSkipLogin.atStartup(required: false, byDefault: true), + isFalse, + ); + }); + }); + + group('SolidSkipLogin.setSkipping', () { + test('keeps a choice that differs from the app default', () async { + await SolidSkipLogin.atStartup(required: false, byDefault: true); + await SolidSkipLogin.setSkipping(false); + + final prefs = await SharedPreferences.getInstance(); + expect(prefs.getBool(SolidSkipLogin.skipPref), isFalse); + expect(SolidSkipLogin.skipping, isFalse); + }); + + test('forgets a choice that matches the app default', () async { + SharedPreferences.setMockInitialValues({SolidSkipLogin.skipPref: false}); + await SolidSkipLogin.atStartup(required: false, byDefault: true); + await SolidSkipLogin.setSkipping(true); + + final prefs = await SharedPreferences.getInstance(); + expect(prefs.containsKey(SolidSkipLogin.skipPref), isFalse); + }); + }); + + group('Login Page settings section', () { + testWidgets('is left out when the app requires a login', (tester) async { + await SolidSkipLogin.atStartup(required: true, byDefault: true); + await _pump(tester); + + expect(find.text('Login Page'), findsNothing); + expect(find.byType(SolidSettingsLoginSection), findsNothing); + }); + + testWidgets('appears, off, when the app skips by default', (tester) async { + await SolidSkipLogin.atStartup(required: false, byDefault: true); + await _pump(tester); + + expect(find.text('Login Page'), findsOneWidget); + expect(_shown(tester), isFalse); + }); + + testWidgets('Save keeps the choice to show the login page', (tester) async { + await SolidSkipLogin.atStartup(required: false, byDefault: true); + await _pump(tester); + + await tester.ensureVisible(find.byType(SolidSettingsLoginSection)); + await tester.tap(find.byType(Switch).last); + await tester.pumpAndSettle(); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + + expect(SolidSkipLogin.skipping, isFalse); + }); + + testWidgets('Default restores the app default', (tester) async { + SharedPreferences.setMockInitialValues({SolidSkipLogin.skipPref: false}); + await SolidSkipLogin.atStartup(required: false, byDefault: true); + await _pump(tester); + + expect(_shown(tester), isTrue); + + await tester.tap(find.text('Default')); + await tester.pumpAndSettle(); + + expect(_shown(tester), isFalse); + }); + }); +}