Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
119 changes: 119 additions & 0 deletions lib/src/utils/solid_skip_login.dart
Original file line number Diff line number Diff line change
@@ -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<bool> 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<void> 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;
}
}
103 changes: 55 additions & 48 deletions lib/src/widgets/solid_login.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -89,6 +90,7 @@ class SolidLogin extends StatefulWidget {
required this.redirectUris,
this.postLogoutRedirectUris = const [],
this.autoLogin = true,
this.skipLogin = false,
super.key,
});

Expand Down Expand Up @@ -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<SolidLogin> createState() => _SolidLoginState();
}
Expand Down Expand Up @@ -233,6 +244,10 @@ class _SolidLoginState extends State<SolidLogin> 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;
Expand Down Expand Up @@ -266,12 +281,11 @@ class _SolidLoginState extends State<SolidLogin> 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.

Expand Down Expand Up @@ -299,26 +313,22 @@ class _SolidLoginState extends State<SolidLogin> 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<void> _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.
Expand Down Expand Up @@ -348,14 +358,15 @@ class _SolidLoginState extends State<SolidLogin> 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<void> _checkAutoLogin() async {
if (!mounted) return;
Future<bool> _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);

Expand Down Expand Up @@ -387,13 +398,13 @@ class _SolidLoginState extends State<SolidLogin> 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
Expand All @@ -414,10 +425,19 @@ class _SolidLoginState extends State<SolidLogin> 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<void> _performContinue() {
_resetDialogCanceledState();
return SolidLoginActions.performContinue(
context: context,
childWidget: widget.child,
defaultFolders: defaultFolders,
Expand Down Expand Up @@ -543,7 +563,7 @@ class _SolidLoginState extends State<SolidLogin> 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()));
}

Expand Down Expand Up @@ -583,19 +603,6 @@ class _SolidLoginState extends State<SolidLogin> with WidgetsBindingObserver {
);
}

Future<void> performContinue() {
_resetDialogCanceledState();
return SolidLoginActions.performContinue(
context: context,
childWidget: widget.child,
defaultFolders: defaultFolders,
defaultFiles: defaultFiles,
updateDialogCanceledState: updateState,
showSnackbar: _showSnackbar,
staySignedIn: _staySignedIn,
);
}

Future<void> performTryAnotherAccount() =>
SolidLoginActions.performTryAnotherAccount(
context: context,
Expand All @@ -620,7 +627,7 @@ class _SolidLoginState extends State<SolidLogin> with WidgetsBindingObserver {
final continueButton = SolidLoginBuildHelper.buildContinueButton(
context: context,
style: widget.continueButtonStyle,
performContinue: performContinue,
performContinue: _performContinue,
focusNode: _continueFocusNode,
);

Expand Down
58 changes: 58 additions & 0 deletions lib/src/widgets/solid_login_auto_config.dart
Original file line number Diff line number Diff line change
@@ -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,
);
}
Loading
Loading