diff --git a/devtools_options.yaml b/devtools_options.yaml
new file mode 100644
index 0000000..fa0b357
--- /dev/null
+++ b/devtools_options.yaml
@@ -0,0 +1,3 @@
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
+extensions:
diff --git a/example/lib/app_scaffold.dart b/example/lib/app_scaffold.dart
index 001ba75..0b07027 100644
--- a/example/lib/app_scaffold.dart
+++ b/example/lib/app_scaffold.dart
@@ -134,19 +134,19 @@ class AppScaffold extends StatelessWidget {
text: '''
This template app demonstrates the following key SolidUI features:
-
+
đ§ Responsive navigation (rail â drawer);
-
+
đ¨ Theme switching (light/dark/system);
-
+
âšī¸ Customisable About dialogues;
-
+
đ Version information display;
-
+
đ Security key management;
-
+
đ Status bar integration;
-
+
đ¤ User information display.
For more information, visit the
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 8d79912..5dc56a7 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -30,9 +30,13 @@ library;
import 'package:flutter/material.dart';
+import 'package:solidpod/solidpod.dart' show KeyManager, setAppDirName;
import 'package:window_manager/window_manager.dart';
+import 'package:solidui/solidui.dart';
+
import 'app.dart';
+import 'app_scaffold.dart';
import 'constants/app.dart';
import 'utils/is_desktop.dart';
@@ -41,6 +45,27 @@ import 'utils/is_desktop.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
+ // CRITICAL: Set app directory name BEFORE any Pod operations.
+
+ await setAppDirName('myapp');
+
+ // Configure SolidAuthHandler with app-specific settings.
+
+ SolidAuthHandler.instance.configure(
+ SolidAuthConfig(
+ appTitle: appTitle,
+ appDirectory: 'myapp',
+ defaultServerUrl: 'https://pods.solidcommunity.au',
+ appImage: const AssetImage('assets/images/app_image.jpg'),
+ appLogo: const AssetImage('assets/images/app_icon.jpg'),
+ loginSuccessWidget: appScaffold,
+ onSecurityKeyReset: () async {
+ await KeyManager.clear();
+ debugPrint('MyApp: Security key cleared on logout');
+ },
+ ),
+ );
+
// Set window options for desktop platforms (Windows, Linux, macOS).
if (isDesktop) {
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 58bfe69..efa8384 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -25,6 +25,8 @@ dependency_overrides:
dev_dependencies:
flutter_lints: ^5.0.0
+ flutter_test:
+ sdk: flutter
flutter:
uses-material-design: true
diff --git a/example/web/callback.html b/example/web/callback.html
new file mode 100644
index 0000000..bb7c891
--- /dev/null
+++ b/example/web/callback.html
@@ -0,0 +1,37 @@
+
+
+
+
+ Authentication Callback
+
+
+
+ Authentication complete. You can close this window.
+ If this window does not close automatically, please close it manually.
+
+
diff --git a/lib/solidui.dart b/lib/solidui.dart
index a09d456..ee63a79 100644
--- a/lib/solidui.dart
+++ b/lib/solidui.dart
@@ -95,6 +95,7 @@ export 'src/utils/solid_file_operations.dart';
export 'src/utils/solid_alert.dart';
export 'src/utils/solid_pod_helpers.dart'
show loginIfRequired, getKeyFromUserIfRequired;
+export 'src/utils/solid_notifications.dart';
export 'src/widgets/solid_format_info_card.dart';
diff --git a/lib/src/handlers/solid_auth_handler.dart b/lib/src/handlers/solid_auth_handler.dart
index e63d285..16ee6c8 100644
--- a/lib/src/handlers/solid_auth_handler.dart
+++ b/lib/src/handlers/solid_auth_handler.dart
@@ -33,7 +33,7 @@ import 'package:flutter/material.dart';
import 'package:solidpod/solidpod.dart' show getWebId;
import 'package:solidui/src/constants/solid_config.dart';
-import 'package:solidui/src/widgets/solid_default_login.dart';
+import 'package:solidui/src/widgets/solid_login.dart';
import 'package:solidui/src/widgets/solid_logout_dialog.dart' show logoutPopup;
/// Configuration for Solid authentication handling.
@@ -61,6 +61,7 @@ class SolidAuthConfig {
final String? appDirectory;
/// App image for the default login page.
+
final AssetImage? appImage;
/// App logo for the default login page.
@@ -114,6 +115,36 @@ class SolidAuthHandler {
_config = config;
}
+ /// Configure default values without overwriting existing configuration.
+ /// This is used by SolidLogin to provide fallback values while preserving
+ /// app-specific settings like onSecurityKeyReset.
+
+ void configureDefaults(SolidAuthConfig defaults) {
+ if (_config == null) {
+ // No existing config, use defaults
+ _config = defaults;
+ } else {
+ // Merge: keep existing non-null values, fill in missing ones from defaults
+ _config = SolidAuthConfig(
+ returnTo: _config!.returnTo ?? defaults.returnTo,
+ loginPageBuilder:
+ _config!.loginPageBuilder ?? defaults.loginPageBuilder,
+ defaultServerUrl:
+ _config!.defaultServerUrl ?? defaults.defaultServerUrl,
+ appTitle: _config!.appTitle ?? defaults.appTitle,
+ appDirectory: _config!.appDirectory ?? defaults.appDirectory,
+ appImage: _config!.appImage ?? defaults.appImage,
+ appLogo: _config!.appLogo ?? defaults.appLogo,
+ appLink: _config!.appLink ?? defaults.appLink,
+ loginSuccessWidget:
+ _config!.loginSuccessWidget ?? defaults.loginSuccessWidget,
+ // IMPORTANT: Preserve app's security key reset callback
+ onSecurityKeyReset:
+ _config!.onSecurityKeyReset ?? defaults.onSecurityKeyReset,
+ );
+ }
+ }
+
/// Handle logout functionality with confirmation popup.
Future handleLogout(BuildContext context) async {
@@ -130,9 +161,12 @@ class SolidAuthHandler {
// No additional navigation needed.
}
- /// Handle login functionality by navigating to login page.
+ /// Handle login functionality - navigates to login page.
+ /// Works consistently across all platforms (web, mobile, desktop).
Future handleLogin(BuildContext context) async {
+ // Navigate to login page using standard Flutter navigation
+ // This works across all platforms and maintains proper widget lifecycle
Navigator.pushReplacement(
context,
MaterialPageRoute(
@@ -142,23 +176,38 @@ class SolidAuthHandler {
}
/// Build the login page widget.
+ ///
+ /// Returns the actual login input page (SolidLogin), not the success page.
+ /// This is used when guest users want to authenticate, or after logout.
Widget _buildLoginPage(BuildContext context) {
if (_config?.loginPageBuilder != null) {
return _config!.loginPageBuilder!(context);
}
- // Use default login page.
+ // Use the login input page, not the success page
+ // The loginSuccessWidget (child) will be shown after successful authentication
+ final mainAppWidget = _config?.loginSuccessWidget ??
+ const Center(child: Text('Authentication required'));
- return SolidDefaultLogin(
- appTitle: _config?.appTitle ?? 'Solid App',
+ // Use ValueKey to identify this as a fresh login page instance
+ // This works with didUpdateWidget() to reset state when needed
+ return SolidLogin(
+ key: const ValueKey('login_page'),
appDirectory: _config?.appDirectory ?? 'solid_app',
- defaultServerUrl:
- _config?.defaultServerUrl ?? SolidConfig.defaultServerUrl,
- appImage: _config?.appImage,
- appLogo: _config?.appLogo,
- appLink: _config?.appLink,
- loginSuccessWidget: _config?.loginSuccessWidget,
+ webID: _config?.defaultServerUrl ?? SolidConfig.defaultServerUrl,
+ // Use provided images or fallback to SolidLogin's defaults from solidpod package
+ image: _config?.appImage ??
+ const AssetImage(
+ 'assets/images/default_image.jpg',
+ package: 'solidpod',
+ ),
+ logo: _config?.appLogo ??
+ const AssetImage(
+ 'assets/images/default_logo.png',
+ package: 'solidpod',
+ ),
+ child: mainAppWidget,
);
}
diff --git a/lib/src/screens/initial_setup_widgets/res_create_form_submission.dart b/lib/src/screens/initial_setup_widgets/res_create_form_submission.dart
index c85e6e6..81c00eb 100644
--- a/lib/src/screens/initial_setup_widgets/res_create_form_submission.dart
+++ b/lib/src/screens/initial_setup_widgets/res_create_form_submission.dart
@@ -57,6 +57,7 @@ ElevatedButton resCreateFormSubmission(
Widget child,
) {
// Use MediaQuery to determine the screen width and adjust the font size accordingly.
+
final screenWidth = MediaQuery.of(context).size.width;
final isSmallDevice =
screenWidth < 360; // A threshold for small devices, can be adjusted.
diff --git a/lib/src/utils/file_operations.dart b/lib/src/utils/file_operations.dart
index 6c5af8f..02af115 100644
--- a/lib/src/utils/file_operations.dart
+++ b/lib/src/utils/file_operations.dart
@@ -110,9 +110,10 @@ class FileOperations {
final fileName = extractResourceName(fileUrl);
- // Skip non-TTL files. Include both .enc.ttl and .ttl files.
+ // Skip ACL files and metadata files, but include all other file types
+ // (TTL, JSON, CSV, TXT, etc.)
- if (!fileName.endsWith('.enc.ttl') && !fileName.endsWith('.ttl')) {
+ if (fileName.endsWith('.acl') || fileName.endsWith('.meta')) {
continue;
}
@@ -182,12 +183,12 @@ class FileOperations {
static Future getDirectoryFileCount(String dirPath) async {
try {
- // Get directory contents and count files.
+ // Get directory contents and count files (exclude ACL and metadata files).
final dirUrl = await getDirUrl(dirPath);
final resources = await getResourcesInContainer(dirUrl);
return resources.files
- .where((f) => f.endsWith('.enc.ttl') || f.endsWith('.ttl'))
+ .where((f) => !f.endsWith('.acl') && !f.endsWith('.meta'))
.length;
} catch (e) {
debugPrint('Error counting files in directory: $e');
diff --git a/lib/src/widgets/build_message_container.dart b/lib/src/widgets/build_message_container.dart
index fcab854..534e0a4 100644
--- a/lib/src/widgets/build_message_container.dart
+++ b/lib/src/widgets/build_message_container.dart
@@ -111,6 +111,7 @@ Container buildMsgBox(
String msg,
) {
// Zheyuan might need to use isRTL in the future
+
// ignore: unused_local_variable
var isRTL = false;
@@ -127,10 +128,12 @@ Container buildMsgBox(
}
// Determine device type for layout adjustments
+
final isMobile = size.width <= 730;
final isTablet = size.width > 730 && size.width <= 1050;
// Minimal horizontal padding for all devices
+
final horizontalPadding =
size.width * 0.01; // Adjust this value to increase or decrease padding
diff --git a/lib/src/widgets/solid_dynamic_login_status.dart b/lib/src/widgets/solid_dynamic_login_status.dart
index 862cd5e..f5b6741 100644
--- a/lib/src/widgets/solid_dynamic_login_status.dart
+++ b/lib/src/widgets/solid_dynamic_login_status.dart
@@ -135,24 +135,30 @@ class _SolidDynamicLoginStatusState extends State {
_currentWebId != null && _currentWebId!.isNotEmpty;
if (isCurrentlyLoggedIn) {
+ // Logout scenario - don't recheck status as page will reload
+
if (widget.onTap != null) {
widget.onTap!.call();
} else {
SolidAuthHandler.instance.handleLogout(context);
}
} else {
+ // Login scenario - can delay status check
+
if (widget.onLogin != null) {
widget.onLogin!.call();
} else {
SolidAuthHandler.instance.handleLogin(context);
}
- }
- Future.delayed(const Duration(milliseconds: 500), () {
- if (mounted) {
- _checkLoginStatus();
- }
- });
+ // Only refresh status for login scenario
+
+ Future.delayed(const Duration(milliseconds: 500), () {
+ if (mounted) {
+ _checkLoginStatus();
+ }
+ });
+ }
}
@override
diff --git a/lib/src/widgets/solid_file_browser.dart b/lib/src/widgets/solid_file_browser.dart
index 77ca40a..ead691e 100644
--- a/lib/src/widgets/solid_file_browser.dart
+++ b/lib/src/widgets/solid_file_browser.dart
@@ -232,10 +232,10 @@ class SolidFileBrowserState extends State {
currentDirDirectoryCount = directories.length;
});
- // Count files in current directory.
+ // Count files in current directory (exclude ACL and metadata files).
currentDirFileCount = resources.files
- .where((f) => f.endsWith('.enc.ttl') || f.endsWith('.ttl'))
+ .where((f) => !f.endsWith('.acl') && !f.endsWith('.meta'))
.length;
// Get file counts for all subdirectories.
diff --git a/lib/src/widgets/solid_format_info_card.dart b/lib/src/widgets/solid_format_info_card.dart
index 1ac1130..7c845f3 100644
--- a/lib/src/widgets/solid_format_info_card.dart
+++ b/lib/src/widgets/solid_format_info_card.dart
@@ -77,6 +77,7 @@ class SolidFormatInfoCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header with icon and title.
+
Row(
children: [
Icon(
@@ -98,6 +99,7 @@ class SolidFormatInfoCard extends StatelessWidget {
),
// Description if provided.
+
if (config.description != null) ...[
const SizedBox(height: 8),
Text(
@@ -111,6 +113,7 @@ class SolidFormatInfoCard extends StatelessWidget {
const SizedBox(height: 12),
// Required fields section.
+
Text(
'Required Fields:',
style: TextStyle(
@@ -137,6 +140,7 @@ class SolidFormatInfoCard extends StatelessWidget {
),
// Optional fields section.
+
if (config.optionalFields.isNotEmpty) ...[
const SizedBox(height: 12),
Text(
@@ -166,6 +170,7 @@ class SolidFormatInfoCard extends StatelessWidget {
],
// Format type indicator.
+
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
@@ -188,6 +193,7 @@ class SolidFormatInfoCard extends StatelessWidget {
),
// Note text.
+
const SizedBox(height: 8),
Text(
config.isJson
diff --git a/lib/src/widgets/solid_login.dart b/lib/src/widgets/solid_login.dart
index 45faafe..4e85b8f 100644
--- a/lib/src/widgets/solid_login.dart
+++ b/lib/src/widgets/solid_login.dart
@@ -43,6 +43,7 @@ import 'package:solidpod/solidpod.dart'
import 'package:url_launcher/url_launcher.dart';
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/widgets/solid_login_auth_handler.dart';
import 'package:solidui/src/widgets/solid_login_buttons.dart';
@@ -61,6 +62,7 @@ class SolidLogin extends StatefulWidget {
const SolidLogin({
// Include the literals here so that they are exposed through the docs.
+
required this.child,
this.required = false,
this.appDirectory = '',
@@ -191,6 +193,15 @@ class _SolidLoginState extends State with WidgetsBindingObserver {
Map defaultFiles = {};
+ // Track the current theme mode.
+ // Always start with light mode regardless of system preference.
+
+ bool isDarkMode = false;
+
+ // Text controller for the URI of the solid server - should be managed in state.
+
+ late TextEditingController _webIdController;
+
// Focus nodes for keyboard navigation.
// Tab order: login -> continue -> register -> info -> server input.
@@ -206,6 +217,16 @@ class _SolidLoginState extends State with WidgetsBindingObserver {
WidgetsBinding.instance.addObserver(this);
solidThemeNotifier.addListener(_onThemeChanged);
+ // Initialise the controller with the widget's webID.
+
+ _webIdController = TextEditingController(text: widget.webID);
+
+ // Auto-configure SolidAuthHandler with this widget's settings
+ // This ensures the handler works even if the app didn't explicitly configure it
+ // Apps can override this by calling configure() in main.dart before runApp().
+
+ _autoConfigureSolidAuthHandler();
+
// Initialise focus nodes for keyboard navigation.
_loginFocusNode = FocusNode(debugLabel: 'loginButton');
@@ -215,12 +236,56 @@ class _SolidLoginState extends State with WidgetsBindingObserver {
_serverInputFocusNode = FocusNode(debugLabel: 'serverInput');
// dc 20251022: please explain why calling an async without await.
+
_initPackageInfo();
}
+ // Auto-configure SolidAuthHandler if not already configured by the app.
+
+ void _autoConfigureSolidAuthHandler() {
+ // Use configureDefaults instead of configure to preserve app settings
+ // This provides working defaults while keeping important app-specific
+ // configurations like onSecurityKeyReset callback.
+
+ SolidAuthHandler.instance.configureDefaults(
+ SolidAuthConfig(
+ appDirectory: widget.appDirectory,
+ defaultServerUrl: widget.webID,
+ appImage: widget.image,
+ appLogo: widget.logo,
+ appLink: widget.link,
+ loginSuccessWidget: widget.child,
+ ),
+ );
+ }
+
+ @override
+ void didUpdateWidget(SolidLogin oldWidget) {
+ super.didUpdateWidget(oldWidget);
+
+ // Always reset the controller text to widget.webID when widget updates
+ // This ensures fresh state when returning from guest mode, even if the user
+ // had manually modified the URL field before leaving
+ // Only skip reset if the current text already matches the intended value.
+
+ if (_webIdController.text != widget.webID) {
+ _webIdController.text = widget.webID;
+ }
+
+ // CRITICAL: Reset appDirName if appDirectory changed
+ // This fixes the double-slash bug when returning from guest mode
+ // Without this, appDirName stays empty causing paths like //data/places.json.
+
+ if (oldWidget.appDirectory != widget.appDirectory) {
+ setAppDirName(widget.appDirectory);
+ }
+ }
+
@override
void dispose() {
- WidgetsBinding.instance.removeObserver(this);
+ // Clean up the controller when the widget is disposed.
+
+ _webIdController.dispose();
solidThemeNotifier.removeListener(_onThemeChanged);
// Dispose focus nodes to avoid memory leaks.
@@ -231,39 +296,28 @@ class _SolidLoginState extends State with WidgetsBindingObserver {
_infoFocusNode.dispose();
_serverInputFocusNode.dispose();
+ WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
- /// Called when the platform brightness changes.
- /// Triggers a rebuild to update the theme when in system mode.
-
- @override
- void didChangePlatformBrightness() {
- if (mounted && solidThemeNotifier.themeMode == ThemeMode.system) {
- setState(() {});
- }
- }
-
- /// Callback when theme notifier changes.
+ // Callback when theme changes from the global notifier.
void _onThemeChanged() {
if (mounted) {
- setState(() {});
- }
- }
-
- /// Determines if dark mode should be used based on the current theme mode.
- /// When in system mode, follows the system brightness.
- /// When explicitly set to light or dark, uses that mode.
-
- bool get isDarkMode {
- switch (solidThemeNotifier.themeMode) {
- case ThemeMode.system:
- return MediaQuery.platformBrightnessOf(context) == Brightness.dark;
- case ThemeMode.light:
- return false;
- case ThemeMode.dark:
- return true;
+ setState(() {
+ // Determine if dark mode based on ThemeMode.
+
+ final themeMode = solidThemeNotifier.themeMode;
+ if (themeMode == ThemeMode.system) {
+ // Follow system brightness.
+
+ final brightness =
+ WidgetsBinding.instance.platformDispatcher.platformBrightness;
+ isDarkMode = brightness == Brightness.dark;
+ } else {
+ isDarkMode = themeMode == ThemeMode.dark;
+ }
+ });
}
}
@@ -389,11 +443,6 @@ class _SolidLoginState extends State with WidgetsBindingObserver {
image: DecorationImage(image: widget.image, fit: BoxFit.cover),
);
- // Text controller for the URI of the solid server to which an authenticate
- // request is sent.
-
- final webIdController = TextEditingController()..text = widget.webID;
-
// Build all buttons using the button builder.
// User input from text field will override the default server URL.
@@ -402,8 +451,8 @@ class _SolidLoginState extends State with WidgetsBindingObserver {
child: SolidLoginButtons.buildRegisterButton(
style: widget.registerButtonStyle,
onPressed: () {
- final webId = webIdController.text.trim().isNotEmpty
- ? webIdController.text.trim()
+ final webId = _webIdController.text.trim().isNotEmpty
+ ? _webIdController.text.trim()
: SolidConfig.defaultServerUrl;
launchUrl(Uri.parse('$webId/.account/login/password/register/'));
},
@@ -416,8 +465,8 @@ class _SolidLoginState extends State with WidgetsBindingObserver {
child: SolidLoginButtons.buildLoginButton(
style: widget.loginButtonStyle,
onPressed: () async {
- final podServer = webIdController.text.trim().isNotEmpty
- ? webIdController.text.trim()
+ final podServer = _webIdController.text.trim().isNotEmpty
+ ? _webIdController.text.trim()
: SolidConfig.defaultServerUrl;
isDialogCanceled = false;
@@ -463,7 +512,7 @@ class _SolidLoginState extends State with WidgetsBindingObserver {
logo: widget.logo,
title: widget.title,
appVersion: appVersion,
- webIdController: webIdController,
+ webIdController: _webIdController,
loginButton: loginButton,
registerButton: registerButton,
continueButton: continueButton,
diff --git a/lib/src/widgets/solid_login_auth_handler.dart b/lib/src/widgets/solid_login_auth_handler.dart
index 22aeebc..82afc7e 100644
--- a/lib/src/widgets/solid_login_auth_handler.dart
+++ b/lib/src/widgets/solid_login_auth_handler.dart
@@ -31,12 +31,13 @@ library;
// ignore_for_file: public_member_api_docs
+import 'dart:async' show unawaited;
+
import 'package:flutter/material.dart';
import 'package:solidpod/solidpod.dart'
show isUserLoggedIn, solidAuthenticate, initialStructureTest;
-import 'package:solidui/src/screens/initial_setup_screen.dart';
import 'package:solidui/src/widgets/solid_animation_dialog.dart';
import 'package:solidui/src/widgets/solid_login_helper.dart';
@@ -99,7 +100,70 @@ class SolidLoginAuthHandler {
// Perform the actual authentication by contacting the server.
if (!context.mounted) return false;
- final authResult = await solidAuthenticate(podServer, context);
+
+ List? authResult;
+ try {
+ authResult = await solidAuthenticate(podServer, context);
+ } catch (e) {
+ // Authentication error - likely server unavailable or network issue
+ debugPrint('SolidLoginAuthHandler: Authentication error: $e');
+
+ if (!context.mounted) return false;
+
+ // Close the animation dialog
+ if (!wasAlreadyLoggedIn) {
+ Navigator.of(context, rootNavigator: true).pop();
+ }
+
+ // Show error dialog with server availability check
+ await showDialog(
+ context: context,
+ barrierDismissible: false,
+ builder: (context) => AlertDialog(
+ title: const Row(
+ children: [
+ Icon(Icons.error_outline, color: Colors.red),
+ SizedBox(width: 8),
+ Text('Connection Failed'),
+ ],
+ ),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ 'Unable to connect to the Solid server.',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 12),
+ Text('Server: $podServer'),
+ const SizedBox(height: 12),
+ const Text('Possible causes:'),
+ const SizedBox(height: 4),
+ const Text('âĸ Server is temporarily unavailable'),
+ const Text('âĸ Network connection issue'),
+ const Text('âĸ Invalid server URL'),
+ const SizedBox(height: 12),
+ Text(
+ 'Error: ${e.toString()}',
+ style: const TextStyle(fontSize: 11, color: Colors.grey),
+ ),
+ ],
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: const Text('OK'),
+ ),
+ ],
+ ),
+ );
+
+ // Navigate back to login screen
+ if (!context.mounted) return false;
+ await pushReplacement(context, originalLoginWidget);
+ return false;
+ }
// If authentication succeeded and the user was already logged in,
// it means they are using a cached session.
@@ -136,28 +200,22 @@ class SolidLoginAuthHandler {
await Future.delayed(const Duration(milliseconds: 300));
}
- // Navigate to the appropriate screen based on structure test.
-
- final resCheckList = await initialStructureTest(
- defaultFolders,
- defaultFiles,
- );
- final allExists = resCheckList.first as bool;
-
+ // Navigate to main app immediately after successful authentication
+ // This provides instant user feedback and better UX
if (!context.mounted) return false;
- if (!allExists) {
- await pushReplacement(
- context,
- InitialSetupScreen(
- resCheckList: resCheckList,
- originalLogin: originalLoginWidget,
- child: childWidget,
- ),
- );
- } else {
- await pushReplacement(context, childWidget);
- }
+ await pushReplacement(context, childWidget);
+
+ // Check initial structure in background (non-blocking)
+ // If setup is needed, user can access it from the app later
+ unawaited(
+ _checkInitialStructureInBackground(
+ defaultFolders,
+ defaultFiles,
+ originalLoginWidget,
+ childWidget,
+ ),
+ );
return true;
} else {
@@ -178,4 +236,46 @@ class SolidLoginAuthHandler {
return false;
}
}
+
+ /// Checks initial POD structure in the background without blocking navigation.
+ ///
+ /// This allows the user to access the app immediately while structure
+ /// verification happens asynchronously. If setup is needed, it can be
+ /// triggered later from within the app.
+ static Future _checkInitialStructureInBackground(
+ List defaultFolders,
+ Map defaultFiles,
+ dynamic originalLoginWidget,
+ Widget childWidget,
+ ) async {
+ try {
+ debugPrint(
+ 'SolidLoginAuthHandler: Checking initial structure in background...',
+ );
+
+ final resCheckList = await initialStructureTest(
+ defaultFolders,
+ defaultFiles,
+ );
+
+ final allExists = resCheckList.first as bool;
+
+ if (allExists) {
+ debugPrint(
+ 'SolidLoginAuthHandler: Initial structure verified successfully',
+ );
+ } else {
+ debugPrint(
+ 'SolidLoginAuthHandler: Initial structure incomplete - user may need to run setup',
+ );
+ // In the future, we could show a notification or prompt here
+ // For now, we just log it and let the user discover setup options in the app
+ }
+ } catch (e) {
+ debugPrint(
+ 'SolidLoginAuthHandler: Background structure check failed: $e',
+ );
+ // Non-critical error - user can still use the app
+ }
+ }
}
diff --git a/lib/src/widgets/solid_login_helper.dart b/lib/src/widgets/solid_login_helper.dart
index 7e1b889..38863ca 100644
--- a/lib/src/widgets/solid_login_helper.dart
+++ b/lib/src/widgets/solid_login_helper.dart
@@ -431,17 +431,18 @@ Widget getThemeToggleTooltip(
}
/// Utility function for navigation
+/// Modified to preserve widget state by using simple replacement instead of clearing entire stack
Future pushReplacement(
BuildContext context,
Widget destinationWidget,
) async {
- Navigator.pushAndRemoveUntil(
- context,
+ // Use simple pushReplacement instead of pushAndRemoveUntil
+ // This preserves the navigation history and doesn't destroy all widgets
+
+ await Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => destinationWidget,
),
- (Route route) =>
- false, // This predicate ensures all previous routes are removed
);
}
diff --git a/lib/src/widgets/solid_logout_dialog.dart b/lib/src/widgets/solid_logout_dialog.dart
index 1be7e7c..e55afeb 100644
--- a/lib/src/widgets/solid_logout_dialog.dart
+++ b/lib/src/widgets/solid_logout_dialog.dart
@@ -55,6 +55,9 @@ class _LogoutDialogState extends State {
child: const Text('OK'),
onPressed: () async {
if (await logoutPod()) {
+ // Navigate to login page after successful logout
+ // Works consistently across all platforms
+
if (context.mounted) {
await Navigator.pushReplacement(
context,
diff --git a/lib/src/widgets/solid_nav_drawer.dart b/lib/src/widgets/solid_nav_drawer.dart
index fa43efd..1966897 100644
--- a/lib/src/widgets/solid_nav_drawer.dart
+++ b/lib/src/widgets/solid_nav_drawer.dart
@@ -162,6 +162,7 @@ class _SolidNavDrawerState extends State {
if (widget.userInfo != null) _buildUserInfoHeader(context, theme),
// Navigation items.
+
Container(
padding: const EdgeInsets.all(NavigationConstants.navDrawerPadding),
child: Column(
@@ -201,10 +202,12 @@ class _SolidNavDrawerState extends State {
}),
// Additional menu items (if provided).
+
if (widget.additionalMenuItems != null)
...widget.additionalMenuItems!,
// Divider and logout option.
+
if (widget.showLogout && widget.onLogout != null) ...[
Divider(
height: NavigationConstants.navDividerHeight,
@@ -268,6 +271,7 @@ class _SolidNavDrawerState extends State {
const Gap(NavigationConstants.userInfoSpacing),
// User name.
+
Text(
user.effectiveUserName,
style: TextStyle(
@@ -278,6 +282,7 @@ class _SolidNavDrawerState extends State {
),
// WebID (if enabled and available).
+
if (user.showWebId &&
user.webId != null &&
user.webId!.isNotEmpty) ...[
diff --git a/lib/src/widgets/solid_nav_models.dart b/lib/src/widgets/solid_nav_models.dart
index cb8127a..4dbc547 100644
--- a/lib/src/widgets/solid_nav_models.dart
+++ b/lib/src/widgets/solid_nav_models.dart
@@ -141,12 +141,14 @@ class SolidNavUserInfo {
}
}
- // Fallback: try to extract from the last slash in the full URL
+ // Fallback: try to extract from the last slash in the full URL.
+
final lastSlashIndex = webId.lastIndexOf('/');
if (lastSlashIndex != -1 && lastSlashIndex < webId.length - 1) {
String candidate = webId.substring(lastSlashIndex + 1);
- // Remove common suffixes
+ // Remove common suffixes.
+
const suffixes = ['profile', 'card#me', '#me'];
for (final suffix in suffixes) {
if (candidate.endsWith(suffix)) {
diff --git a/lib/src/widgets/solid_scaffold.dart b/lib/src/widgets/solid_scaffold.dart
index bb63eb1..9ff5a44 100644
--- a/lib/src/widgets/solid_scaffold.dart
+++ b/lib/src/widgets/solid_scaffold.dart
@@ -502,6 +502,7 @@ class SolidScaffoldState extends State {
),
_onMenuSelected,
widget.onShowAlert,
+ widget.menu, // Pass menu items for IndexedStack
);
return NotificationListener(
onNotification: (notification) {
@@ -525,6 +526,7 @@ class SolidScaffoldState extends State {
isCompatibilityMode: isCompatibilityMode,
bodyContent: bodyContent,
isKeySaved: _isKeySaved,
+ isLoadingSecurityKey: _securityKeyHelper?.isUpdating ?? false,
currentSelectedIndex: _currentSelectedIndex,
onMenuSelected: _onMenuSelected,
getUsesInternalManagement: _getUsesInternalManagement,
diff --git a/lib/src/widgets/solid_scaffold_appbar_overflow.dart b/lib/src/widgets/solid_scaffold_appbar_overflow.dart
index 86fd20f..430081f 100644
--- a/lib/src/widgets/solid_scaffold_appbar_overflow.dart
+++ b/lib/src/widgets/solid_scaffold_appbar_overflow.dart
@@ -113,10 +113,10 @@ class SolidAppBarOverflowHandler {
final isInOverflow = actionConfig?.showInOverflow ?? false;
- // On narrow screens, only show in overflow if showInOverflow = true.
- // Buttons marked as "add to appbar" (showInOverflow = false) stay in AppBar.
+ // On narrow screens (forceOverflow = true), always show visible buttons
+ // in overflow menu regardless of showInOverflow setting.
- if (forceOverflow) return isInOverflow;
+ if (forceOverflow) return true;
return isInOverflow;
}
@@ -135,10 +135,10 @@ class SolidAppBarOverflowHandler {
final isInOverflow = actionConfig?.showInOverflow ?? false;
- // On narrow screens, only show in overflow if showInOverflow = true.
- // Buttons marked as "add to appbar" (showInOverflow = false) stay in AppBar.
+ // On narrow screens (forceOverflow = true), always show visible buttons
+ // in overflow menu regardless of showInOverflow setting.
- if (forceOverflow) return isInOverflow;
+ if (forceOverflow) return true;
return isInOverflow;
}
@@ -156,10 +156,10 @@ class SolidAppBarOverflowHandler {
final isInOverflow = actionConfig?.showInOverflow ?? false;
- // On narrow screens, only show in overflow if showInOverflow = true.
- // Buttons marked as "add to appbar" (showInOverflow = false) stay in AppBar.
+ // On narrow screens (forceOverflow = true), always show visible buttons
+ // in overflow menu regardless of showInOverflow setting.
- if (forceOverflow) return isInOverflow;
+ if (forceOverflow) return true;
return isInOverflow;
}
@@ -177,10 +177,10 @@ class SolidAppBarOverflowHandler {
final isInOverflow = actionConfig?.showInOverflow ?? false;
- // On narrow screens, only show in overflow if showInOverflow = true.
- // Buttons marked as "add to appbar" (showInOverflow = false) stay in AppBar.
+ // On narrow screens (forceOverflow = true), always show visible buttons
+ // in overflow menu regardless of showInOverflow setting.
- if (forceOverflow) return isInOverflow;
+ if (forceOverflow) return true;
return isInOverflow;
}
diff --git a/lib/src/widgets/solid_scaffold_build_helper.dart b/lib/src/widgets/solid_scaffold_build_helper.dart
index 74b0d0b..d1573ca 100644
--- a/lib/src/widgets/solid_scaffold_build_helper.dart
+++ b/lib/src/widgets/solid_scaffold_build_helper.dart
@@ -124,6 +124,7 @@ class SolidScaffoldBuildHelper {
required bool isCompatibilityMode,
required Widget? bodyContent,
required bool isKeySaved,
+ bool isLoadingSecurityKey = false,
required int currentSelectedIndex,
required void Function(int) onMenuSelected,
required bool Function() getUsesInternalManagement,
@@ -182,12 +183,10 @@ class SolidScaffoldBuildHelper {
bodyContent: bodyContent,
bottomNavigationBar: isCompatibilityMode
? config.bottomNavigationBar
- : (config.hideNavRail
- ? null
- : SolidScaffoldLayoutBuilder.buildStatusBar(
- config.statusBar,
- isKeySaved,
- )),
+ : SolidScaffoldLayoutBuilder.buildStatusBar(
+ config.statusBar,
+ isKeySaved,
+ ),
bottomSheet: config.bottomSheet,
persistentFooterButtons: config.persistentFooterButtons,
resizeToAvoidBottomInset: config.resizeToAvoidBottomInset,
diff --git a/lib/src/widgets/solid_scaffold_layout_builder.dart b/lib/src/widgets/solid_scaffold_layout_builder.dart
index 8b356c1..0b8ff0a 100644
--- a/lib/src/widgets/solid_scaffold_layout_builder.dart
+++ b/lib/src/widgets/solid_scaffold_layout_builder.dart
@@ -36,13 +36,15 @@ import 'package:solidui/src/constants/navigation.dart';
import 'package:solidui/src/widgets/solid_dynamic_login_status.dart';
import 'package:solidui/src/widgets/solid_nav_bar.dart';
import 'package:solidui/src/widgets/solid_nav_models.dart';
+import 'package:solidui/src/widgets/solid_scaffold_models.dart';
import 'package:solidui/src/widgets/solid_status_bar.dart';
import 'package:solidui/src/widgets/solid_status_bar_models.dart';
/// Builder class for creating Scaffold layouts.
class SolidScaffoldLayoutBuilder {
- /// Builds the main body content.
+ /// Builds the main body content using IndexedStack to preserve widget state.
+ /// This prevents unnecessary widget rebuilds when switching between tabs.
static Widget buildBody(
BuildContext context,
@@ -52,6 +54,7 @@ class SolidScaffoldLayoutBuilder {
Widget? effectiveChild,
Function(int) onTabSelected,
Function(BuildContext, String, String?)? onShowAlert,
+ List? menuItems,
) {
final theme = Theme.of(context);
@@ -59,6 +62,21 @@ class SolidScaffoldLayoutBuilder {
return const SizedBox.shrink();
}
+ // Build IndexedStack with all menu item children to preserve state
+ Widget contentArea;
+ if (menuItems != null && menuItems.isNotEmpty) {
+ contentArea = IndexedStack(
+ index: (selectedIndex ?? 0).clamp(0, menuItems.length - 1),
+ sizing: StackFit.expand,
+ children: menuItems
+ .map((item) => item.child ?? const SizedBox.shrink())
+ .toList(),
+ );
+ } else {
+ // Fallback to single child if no menu items
+ contentArea = effectiveChild;
+ }
+
if (isWideScreen) {
// Wide screen: show navigation bar + content.
@@ -75,7 +93,7 @@ class SolidScaffoldLayoutBuilder {
onShowAlert: onShowAlert,
),
VerticalDivider(width: 1, color: theme.dividerColor),
- Expanded(child: effectiveChild),
+ Expanded(child: contentArea),
],
),
),
@@ -87,7 +105,7 @@ class SolidScaffoldLayoutBuilder {
return Column(
children: [
Divider(height: 1, color: theme.dividerColor),
- Expanded(child: effectiveChild),
+ Expanded(child: contentArea),
],
);
}
@@ -95,7 +113,11 @@ class SolidScaffoldLayoutBuilder {
/// Builds the status bar.
- static Widget? buildStatusBar(SolidStatusBarConfig? config, bool isKeySaved) {
+ static Widget? buildStatusBar(
+ SolidStatusBarConfig? config,
+ bool isKeySaved, {
+ bool isLoading = false,
+ }) {
if (config == null) return null;
// Create a modified config with updated security key status.
@@ -106,12 +128,14 @@ class SolidScaffoldLayoutBuilder {
final originalStatus = config.securityKeyStatus!;
final updatedStatus = SolidSecurityKeyStatus(
isKeySaved: isKeySaved,
+ isLoading: isLoading,
onTap: originalStatus.onTap,
onKeyStatusChanged: originalStatus.onKeyStatusChanged,
title: originalStatus.title,
appWidget: originalStatus.appWidget,
keySavedText: originalStatus.keySavedText,
keyNotSavedText: originalStatus.keyNotSavedText,
+ loadingText: originalStatus.loadingText,
tooltip: originalStatus.tooltip,
);
diff --git a/lib/src/widgets/solid_scaffold_widget_builder.dart b/lib/src/widgets/solid_scaffold_widget_builder.dart
index a6085e7..2cfabaf 100644
--- a/lib/src/widgets/solid_scaffold_widget_builder.dart
+++ b/lib/src/widgets/solid_scaffold_widget_builder.dart
@@ -75,6 +75,7 @@ class SolidScaffoldWidgetBuilder {
required bool isCompatibilityMode,
required Widget? bodyContent,
required bool isKeySaved,
+ required bool isLoadingSecurityKey,
required int? currentSelectedIndex,
required void Function(int) onMenuSelected,
required bool Function() getUsesInternalManagement,
@@ -140,12 +141,11 @@ class SolidScaffoldWidgetBuilder {
bodyContent: bodyContent,
bottomNavigationBar: isCompatibilityMode
? widget.bottomNavigationBar
- : (widget.hideNavRail
- ? null
- : SolidScaffoldLayoutBuilder.buildStatusBar(
- widget.statusBar,
- isKeySaved,
- )),
+ : SolidScaffoldLayoutBuilder.buildStatusBar(
+ widget.statusBar,
+ isKeySaved,
+ isLoading: isLoadingSecurityKey,
+ ),
bottomSheet: widget.bottomSheet,
persistentFooterButtons: widget.persistentFooterButtons,
resizeToAvoidBottomInset: widget.resizeToAvoidBottomInset,
diff --git a/lib/src/widgets/solid_security_key_manager.dart b/lib/src/widgets/solid_security_key_manager.dart
index df37d81..cd2da73 100644
--- a/lib/src/widgets/solid_security_key_manager.dart
+++ b/lib/src/widgets/solid_security_key_manager.dart
@@ -30,8 +30,14 @@ library;
import 'package:flutter/material.dart';
-import 'package:solidpod/solidpod.dart'
- show KeyManager, deleteFile, getEncKeyPath;
+import 'package:solidpod/solidpod.dart' as solidpod
+ show
+ KeyManager,
+ deleteFile,
+ getEncKeyPath,
+ getFileUrl,
+ checkResourceStatus,
+ ResourceStatus;
import 'package:solidui/src/services/solid_security_key_notifier.dart';
import 'package:solidui/src/widgets/solid_security_key_manager_dialogs.dart';
@@ -197,9 +203,54 @@ class SolidSecurityKeyManagerState extends State
);
return;
}
+
+ // CRITICAL FIX: Check if server has enc-keys.ttl before showing new key dialog.
+ // If server has keys but local doesn't, user needs to RESTORE key, not create new.
+ // Creating new would overwrite server keys and cause permanent data loss!
+
+ try {
+ final encKeyPath = await solidpod.getEncKeyPath();
+ final encKeyUrl = await solidpod.getFileUrl(encKeyPath);
+ final status =
+ await solidpod.checkResourceStatus(encKeyUrl, isFile: true);
+
+ if (status == solidpod.ResourceStatus.exist) {
+ // Server has keys - show restore key dialog instead of new key dialog.
+ if (!context.mounted) return;
+ await _showRestoreKeyDialog(context);
+ return;
+ }
+ } catch (e) {
+ debugPrint('Error checking server key status: $e');
+ // On error, fall through to new key dialog (safer default for new users).
+ }
+
+ if (!context.mounted) return;
return _showNewKeyDialog(context);
}
+ /// Shows dialog to restore/verify an existing security key.
+ /// Used when server has enc-keys.ttl but local storage doesn't have the key.
+ Future _showRestoreKeyDialog(BuildContext context) async {
+ await SolidSecurityKeyManagerDialogs.showRestoreKeyDialog(
+ context,
+ _keyController,
+ () async {
+ _updateKeyStatusAfterSet();
+ },
+ (key) async {
+ return await SecurityKeyOperations.handleRestoreKey(
+ key,
+ (message) {
+ if (mounted) {
+ SecurityKeyUIHelpers.showErrorSnackBar(this.context, message);
+ }
+ },
+ );
+ },
+ );
+ }
+
Future _showNewKeyDialog(BuildContext context) async {
await SolidSecurityKeyManagerDialogs.showNewKeyDialog(
context,
@@ -253,7 +304,7 @@ class SolidSecurityKeyManagerState extends State
'The security key file could not be found. '
'Would you like to set a new security key?',
);
- await KeyManager.forgetSecurityKey();
+ await solidpod.KeyManager.forgetSecurityKey();
await _checkKeyStatus();
if (context.mounted) {
await _showKeyInputDialog(context);
@@ -287,12 +338,14 @@ class SolidSecurityKeyManagerState extends State
try {
// Clear the key from memory.
- await KeyManager.forgetSecurityKey();
+ await solidpod.KeyManager.forgetSecurityKey();
// Delete the key file from POD.
+ // IMPORTANT: Use isKey: true to skip permission revocation
+ // because encryption files are NOT in the data directory
- final encKeyPath = await getEncKeyPath();
- await deleteFile(encKeyPath);
+ final encKeyPath = await solidpod.getEncKeyPath();
+ await solidpod.deleteFile(encKeyPath, isKey: true);
success = true;
msg = 'Successfully forgot local security key.';
diff --git a/lib/src/widgets/solid_security_key_manager_dialogs.dart b/lib/src/widgets/solid_security_key_manager_dialogs.dart
index d67ad35..2788ffd 100644
--- a/lib/src/widgets/solid_security_key_manager_dialogs.dart
+++ b/lib/src/widgets/solid_security_key_manager_dialogs.dart
@@ -94,6 +94,113 @@ class SolidSecurityKeyManagerDialogs {
);
}
+ /// Shows the restore key dialog for verifying an existing security key.
+ /// Used when server has enc-keys.ttl but local storage doesn't have the key.
+
+ static Future showRestoreKeyDialog(
+ BuildContext context,
+ TextEditingController keyController,
+ Future Function() onKeyRestored,
+ Future Function(String key) handleRestoreFunction,
+ ) async {
+ keyController.clear();
+ bool isLoading = false;
+ bool obscureText = true;
+
+ return showDialog(
+ context: context,
+ barrierDismissible: true,
+ builder: (context) => StatefulBuilder(
+ builder: (context, setState) => AlertDialog(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ title: const Row(
+ children: [
+ Icon(Icons.key, color: Colors.orange),
+ SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ 'Restore Security Key',
+ style: TextStyle(fontSize: 18),
+ ),
+ ),
+ ],
+ ),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ 'Your encrypted data was found on the server.',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Please enter the security key you originally set to restore '
+ 'access to your encrypted data.',
+ style: TextStyle(fontSize: 13),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'â ī¸ If you forgot your key, your encrypted data cannot be recovered.',
+ style: TextStyle(
+ fontSize: 12,
+ color: Colors.red,
+ fontStyle: FontStyle.italic,
+ ),
+ ),
+ const SizedBox(height: 16),
+ TextField(
+ controller: keyController,
+ obscureText: obscureText,
+ decoration: InputDecoration(
+ labelText: 'Security Key',
+ border: const OutlineInputBorder(),
+ suffixIcon: IconButton(
+ icon: Icon(
+ obscureText ? Icons.visibility_off : Icons.visibility,
+ ),
+ onPressed: () => setState(() => obscureText = !obscureText),
+ ),
+ ),
+ enabled: !isLoading,
+ ),
+ ],
+ ),
+ actions: [
+ TextButton(
+ onPressed: isLoading ? null : () => Navigator.pop(context),
+ child: const Text('Cancel'),
+ ),
+ ElevatedButton(
+ onPressed: isLoading
+ ? null
+ : () async {
+ setState(() => isLoading = true);
+ final success =
+ await handleRestoreFunction(keyController.text);
+ if (success) {
+ if (context.mounted) Navigator.pop(context);
+ await onKeyRestored();
+ } else {
+ setState(() => isLoading = false);
+ }
+ },
+ child: isLoading
+ ? const SizedBox(
+ width: 16,
+ height: 16,
+ child: CircularProgressIndicator(strokeWidth: 2),
+ )
+ : const Text('Restore'),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
/// Shows the key file not found dialogue.
static Future showKeyFileNotFoundDialog(
diff --git a/lib/src/widgets/solid_security_key_operations.dart b/lib/src/widgets/solid_security_key_operations.dart
index 7272f98..13cbddb 100644
--- a/lib/src/widgets/solid_security_key_operations.dart
+++ b/lib/src/widgets/solid_security_key_operations.dart
@@ -71,9 +71,8 @@ class SecurityKeyOperations {
try {
// Use initPodKeys() to re-initialise the security key.
-
- // await KeyManager.initPodKeys(key);
- // debugPrint('Security key successfully initialised.');
+ await KeyManager.initPodKeys(key);
+ debugPrint('Security key successfully initialised and saved.');
return true;
} catch (e) {
debugPrint('Error setting security key: $e');
@@ -107,4 +106,52 @@ class SecurityKeyOperations {
return false;
}
}
+
+ /// Handles restoring/verifying an existing security key.
+ /// This is used when the server has enc-keys.ttl but local storage doesn't.
+ /// Unlike handleKeySubmission, this uses setSecurityKey which VERIFIES
+ /// against the server's verification key instead of overwriting it.
+
+ static Future handleRestoreKey(
+ String key,
+ void Function(String message) showErrorFunction,
+ ) async {
+ if (key.isEmpty) {
+ showErrorFunction('Please enter your security key');
+ return false;
+ }
+
+ try {
+ // Use setSecurityKey() which verifies against server's verification key.
+ // This will throw if the key doesn't match.
+ await KeyManager.setSecurityKey(key);
+ debugPrint('Security key successfully restored and verified.');
+ return true;
+ } catch (e) {
+ debugPrint('Error restoring security key: $e');
+ String errorMessage = 'Incorrect security key.';
+
+ final errorStr = e.toString().toLowerCase();
+
+ if (errorStr.contains('not logged in') ||
+ errorStr.contains('authentication')) {
+ errorMessage = 'You must be logged in to restore your security key.';
+ } else if (errorStr.contains('network') ||
+ errorStr.contains('connection')) {
+ errorMessage =
+ 'Network error. Please check your connection and try again.';
+ } else if (errorStr.contains('verify')) {
+ errorMessage =
+ 'Incorrect security key. Please enter the key you originally set.';
+ }
+
+ try {
+ showErrorFunction(errorMessage);
+ } catch (displayError) {
+ debugPrint('Error displaying error message: $displayError');
+ }
+
+ return false;
+ }
+ }
}
diff --git a/lib/src/widgets/solid_status_bar.dart b/lib/src/widgets/solid_status_bar.dart
index f300cb0..e6e3b7e 100644
--- a/lib/src/widgets/solid_status_bar.dart
+++ b/lib/src/widgets/solid_status_bar.dart
@@ -34,6 +34,7 @@ import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:markdown_tooltip/markdown_tooltip.dart';
+import 'package:solidpod/solidpod.dart' show getWebId;
import 'package:url_launcher/url_launcher.dart';
import 'package:solidui/src/constants/navigation.dart';
@@ -159,6 +160,33 @@ class SolidStatusBar extends StatelessWidget {
final theme = Theme.of(context);
+ // Show loading indicator if status is being loaded.
+
+ if (securityKeyStatus.isLoading) {
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ SizedBox(
+ width: 14,
+ height: 14,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ valueColor: AlwaysStoppedAnimation(
+ theme.colorScheme.primary,
+ ),
+ ),
+ ),
+ const SizedBox(width: 8),
+ Text(
+ securityKeyStatus.displayText,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.primary,
+ ),
+ ),
+ ],
+ );
+ }
+
// Determine the onTap handler.
VoidCallback? onTap = securityKeyStatus.onTap;
@@ -184,10 +212,56 @@ class SolidStatusBar extends StatelessWidget {
/// Shows the built-in security key manager dialogue.
- void _showSecurityKeyManager(
+ Future _showSecurityKeyManager(
BuildContext context,
SolidSecurityKeyStatus config,
- ) {
+ ) async {
+ // Import at top: import 'package:solidpod/solidpod.dart' show getWebId;
+ // Check if user is logged in first.
+
+ try {
+ final webId = await getWebId();
+
+ if (webId == null || webId.isEmpty) {
+ // Show friendly login prompt.
+
+ if (!context.mounted) return;
+ await showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ title: const Row(
+ children: [
+ Icon(Icons.info_outline, color: Colors.blue),
+ SizedBox(width: 8),
+ Text('Login Required'),
+ ],
+ ),
+ content: const Text(
+ 'You must be logged in to manage your security key.\n\n'
+ 'Security keys are used to encrypt your sensitive data stored in your Solid Pod.\n\n'
+ 'Please log in first, then you can set up your security key.',
+ style: TextStyle(fontSize: 14),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('OK'),
+ ),
+ ],
+ ),
+ );
+ return;
+ }
+ } catch (e) {
+ debugPrint('Error checking login status: $e');
+ // Continue to show dialog anyway
+ }
+
+ if (!context.mounted) return;
+
showDialog(
context: context,
barrierColor: Colors.black.withValues(alpha: 0.5),
diff --git a/lib/src/widgets/solid_status_bar_models.dart b/lib/src/widgets/solid_status_bar_models.dart
index 16fc554..cad6819 100644
--- a/lib/src/widgets/solid_status_bar_models.dart
+++ b/lib/src/widgets/solid_status_bar_models.dart
@@ -322,6 +322,10 @@ class SolidSecurityKeyStatus {
final bool? isKeySaved;
+ /// Whether the security key status is currently loading.
+
+ final bool isLoading;
+
/// Optional callback when security key management is tapped.
/// If null, SolidScaffold will handle security key management automatically.
@@ -349,25 +353,33 @@ class SolidSecurityKeyStatus {
final String? keyNotSavedText;
+ /// Custom text for loading state (if null, uses default).
+
+ final String? loadingText;
+
/// Tooltip message for the security key status.
final String? tooltip;
const SolidSecurityKeyStatus({
this.isKeySaved,
+ this.isLoading = false,
this.onTap,
this.onKeyStatusChanged,
this.title,
this.appWidget,
this.keySavedText,
this.keyNotSavedText,
+ this.loadingText,
this.tooltip,
});
/// Get the display text based on key status.
String get displayText {
- if (isKeySaved == true) {
+ if (isLoading) {
+ return loadingText ?? 'Security Key: Loading...';
+ } else if (isKeySaved == true) {
return keySavedText ?? 'Security Key: Saved';
} else {
return keyNotSavedText ?? 'Security Key: Not Saved';
diff --git a/pubspec.yaml b/pubspec.yaml
index 8cd8734..7fba6f8 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -40,7 +40,9 @@ dependency_overrides:
dev_dependencies:
flutter_lints: ^6.0.0
+ flutter_test:
+ sdk: flutter
window_manager: ^0.5.1
flutter:
- uses-material-design: true
+ uses-material-design: true
\ No newline at end of file