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
1 change: 1 addition & 0 deletions lib/solidui.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export 'src/widgets/solid_scaffold_models.dart';
export 'src/widgets/solid_status_bar.dart';
export 'src/widgets/solid_status_bar_models.dart';
export 'src/widgets/solid_dynamic_login_status.dart';
export 'src/widgets/solid_dynamic_auth_button.dart';
export 'src/widgets/solid_default_login.dart';

export 'src/widgets/solid_login.dart';
Expand Down
59 changes: 58 additions & 1 deletion lib/src/handlers/solid_auth_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@ class SolidAuthHandler {
static SolidAuthHandler? _instance;
SolidAuthConfig? _config;

// Cached login configuration from the app's original SolidLogin widget.

String? _cachedTitle;
String? _cachedAppDirectory;
String? _cachedWebId;
AssetImage? _cachedImage;
AssetImage? _cachedLogo;
String? _cachedLink;
Widget? _cachedChild;
bool _isAutoConfigured = false;

SolidAuthHandler._internal();

/// Singleton instance of the authentication handler.
Expand All @@ -114,6 +125,32 @@ class SolidAuthHandler {
_config = config;
}

/// Auto-configure from SolidLogin widget parameters.
/// Called automatically when SolidLogin initialises.

void autoConfigureFromLogin({
required String title,
required String appDirectory,
required String webId,
required AssetImage image,
required AssetImage logo,
required String link,
required Widget child,
}) {
_cachedTitle = title;
_cachedAppDirectory = appDirectory;
_cachedWebId = webId;
_cachedImage = image;
_cachedLogo = logo;
_cachedLink = link;
_cachedChild = child;
_isAutoConfigured = true;
}

/// Check if auto-configuration is available.

bool get hasAutoConfig => _isAutoConfigured;

/// Handle logout functionality with confirmation popup.

Future<void> handleLogout(BuildContext context) async {
Expand All @@ -131,6 +168,7 @@ class SolidAuthHandler {
}

/// Handle login functionality by navigating to login page.
/// After successful login, navigates back to the app's root route.

Future<void> handleLogin(BuildContext context) async {
Navigator.pushReplacement(
Expand All @@ -148,7 +186,25 @@ class SolidAuthHandler {
return _config!.loginPageBuilder!(context);
}

// Use default login page.
// Use auto-configured values from the app's original SolidLogin if
// available.

if (_isAutoConfigured && _cachedChild != null) {
return SolidDefaultLogin(
appTitle: _cachedTitle ?? _config?.appTitle ?? 'Solid App',
appDirectory:
_cachedAppDirectory ?? _config?.appDirectory ?? 'solid_app',
defaultServerUrl: _cachedWebId ??
_config?.defaultServerUrl ??
SolidConfig.defaultServerUrl,
appImage: _cachedImage ?? _config?.appImage,
appLogo: _cachedLogo ?? _config?.appLogo,
appLink: _cachedLink ?? _config?.appLink,
loginSuccessWidget: _config?.loginSuccessWidget ?? _cachedChild,
);
}

// Fall back to manual configuration or defaults.

return SolidDefaultLogin(
appTitle: _config?.appTitle ?? 'Solid App',
Expand All @@ -159,6 +215,7 @@ class SolidAuthHandler {
appLogo: _config?.appLogo,
appLink: _config?.appLink,
loginSuccessWidget: _config?.loginSuccessWidget,
navigateToRootOnSuccess: _config?.loginSuccessWidget == null,
);
}

Expand Down
70 changes: 68 additions & 2 deletions lib/src/widgets/solid_default_login.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,15 @@ class SolidDefaultLogin extends StatelessWidget {

/// Widget to navigate to after successful login.
///
/// If not provided, a default success screen will be shown.
/// If not provided and [navigateToRootOnSuccess] is false, a default
/// success screen will be shown.

final Widget? loginSuccessWidget;

/// Whether to navigate to the app's root route ('/') after successful login.

final bool navigateToRootOnSuccess;

const SolidDefaultLogin({
super.key,
required this.appTitle,
Expand All @@ -81,10 +86,24 @@ class SolidDefaultLogin extends StatelessWidget {
this.appLogo,
this.appLink,
this.loginSuccessWidget,
this.navigateToRootOnSuccess = false,
});

@override
Widget build(BuildContext context) {
// Determine the success widget based on configuration.

Widget successWidget;
if (loginSuccessWidget != null) {
successWidget = loginSuccessWidget!;
} else if (navigateToRootOnSuccess) {
// Navigate to root route after login, returning to app's entry point.

successWidget = _RootNavigator(appTitle: appTitle);
} else {
successWidget = _buildDefaultSuccessWidget(context);
}

return Theme(
data: Theme.of(context).brightness == Brightness.dark
? ThemeData.dark()
Expand All @@ -97,7 +116,7 @@ class SolidDefaultLogin extends StatelessWidget {
image: appImage ?? SolidConfig.defaultImage,
logo: appLogo ?? SolidConfig.defaultLogo,
link: appLink ?? '',
child: loginSuccessWidget ?? _buildDefaultSuccessWidget(context),
child: successWidget,
),
);
}
Expand Down Expand Up @@ -141,3 +160,50 @@ class SolidDefaultLogin extends StatelessWidget {
);
}
}

/// A widget that navigates to the app's root route when built.
/// Used as the login success destination to return to the app's entry point.

class _RootNavigator extends StatefulWidget {
final String appTitle;

const _RootNavigator({required this.appTitle});

@override
State<_RootNavigator> createState() => _RootNavigatorState();
}

class _RootNavigatorState extends State<_RootNavigator> {
@override
void initState() {
super.initState();
// Navigate to root route after the widget is built.

WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false);
}
});
}

@override
Widget build(BuildContext context) {
// Show a brief loading indicator whilst navigating.

return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(
'Logging in to ${widget.appTitle}...',
style: const TextStyle(fontSize: 16),
),
],
),
),
);
}
}
181 changes: 181 additions & 0 deletions lib/src/widgets/solid_dynamic_auth_button.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/// Dynamic Authentication Button Widget.
///
/// 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: Tony Chen

library;

import 'package:flutter/material.dart';

import 'package:markdown_tooltip/markdown_tooltip.dart';
import 'package:solidpod/solidpod.dart' show getWebId, isUserLoggedIn;

import 'package:solidui/src/handlers/solid_auth_handler.dart';

/// A dynamic authentication button that shows login or logout icon
/// based on the current authentication state.
///
/// When user is logged out: shows login icon, navigates to login page on tap.
/// When user is logged in: shows logout icon, triggers logout on tap.

class SolidDynamicAuthButton extends StatefulWidget {
/// Callback function triggered when user taps logout (when logged in).
/// If null, uses SolidAuthHandler.instance.handleLogout().

final void Function(BuildContext)? onLogout;

/// Callback function triggered when user taps login (when logged out).
/// If null, uses SolidAuthHandler.instance.handleLogin().

final void Function(BuildContext)? onLogin;

/// Tooltip message for the login button.

final String loginTooltip;

/// Tooltip message for the logout button.

final String logoutTooltip;

const SolidDynamicAuthButton({
super.key,
this.onLogout,
this.onLogin,
this.loginTooltip = 'Log in to your Solid POD',
this.logoutTooltip = 'Log out of the current session',
});

@override
State<SolidDynamicAuthButton> createState() => _SolidDynamicAuthButtonState();
}

class _SolidDynamicAuthButtonState extends State<SolidDynamicAuthButton> {
bool _isLoggedIn = false;
bool _isLoading = true;

@override
void initState() {
super.initState();
_checkLoginStatus();
}

/// Checks the current login status by verifying the WebID and session state.

Future<void> _checkLoginStatus() async {
try {
final webId = await getWebId();

if (webId == null || webId.isEmpty) {
if (mounted) {
setState(() {
_isLoggedIn = false;
_isLoading = false;
});
}
return;
}

// Verify if the user is actually logged in with a valid session.

final isLoggedIn = await isUserLoggedIn();

if (mounted) {
setState(() {
_isLoggedIn = isLoggedIn;
_isLoading = false;
});
}
} catch (e) {
debugPrint('Error checking login status: $e');
if (mounted) {
setState(() {
_isLoggedIn = false;
_isLoading = false;
});
}
}
}

/// Handles button tap based on current authentication state.

Future<void> _handleTap() async {
if (_isLoggedIn) {
// User is logged in, perform logout.

if (widget.onLogout != null) {
widget.onLogout!(context);
} else {
await SolidAuthHandler.instance.handleLogout(context);
}
} else {
// User is not logged in, navigate to login page.

if (widget.onLogin != null) {
widget.onLogin!(context);
} else {
await SolidAuthHandler.instance.handleLogin(context);
}
}

// Refresh the login status after a brief delay.

Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) {
_checkLoginStatus();
}
});
}

@override
Widget build(BuildContext context) {
if (_isLoading) {
// Show a subtle loading indicator whilst checking status.

return const SizedBox(
width: 48,
height: 48,
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
);
}

final icon = _isLoggedIn ? Icons.logout : Icons.login;
final tooltip = _isLoggedIn ? widget.logoutTooltip : widget.loginTooltip;

return MarkdownTooltip(
message: tooltip,
child: IconButton(
icon: Icon(icon),
onPressed: _handleTap,
),
);
}
}
Loading