diff --git a/example/.metadata b/example/.metadata index a8d40816..df13aa7f 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "20f82749394e68bcfbbeee96bad384abaae09c13" + revision: "ff37bef603469fb030f2b72995ab929ccfc227f0" channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: 20f82749394e68bcfbbeee96bad384abaae09c13 - base_revision: 20f82749394e68bcfbbeee96bad384abaae09c13 - - platform: linux - create_revision: 20f82749394e68bcfbbeee96bad384abaae09c13 - base_revision: 20f82749394e68bcfbbeee96bad384abaae09c13 + create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 + base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 + - platform: android + create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 + base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 # User provided section diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 00000000..0d290213 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/example/lib/app.dart b/example/lib/app.dart index 107b9353..ceec376b 100644 --- a/example/lib/app.dart +++ b/example/lib/app.dart @@ -45,15 +45,28 @@ class App extends StatelessWidget { useMaterial3: true, ), home: const SolidLogin( - title: 'SOLID POD DEMONSTRATOR', + // Images generated using Bing Image Creator from Designer, powered by + // DALL-E3. + + title: 'SOLID UI DEMONSTRATOR', appDirectory: 'demopod', - image: AssetImage('assets/images/demopod_image.jpg'), + image: AssetImage('assets/images/demopod_image.png'), logo: AssetImage('assets/images/demopod_logo.png'), link: 'https://github.com/anusii/solidpod/blob/main/demopod/README.md', required: false, infoButtonStyle: InfoButtonStyle( tooltip: 'Visit the DemoPod documentation.', ), + clientId: + 'https://anushkavidanage.github.io/solidui/example/client-profile.jsonld', + // Use the following schemas depending on the platform + // Web: https://anushkavidanage.github.io/solidpod/example/redirect.html + // Mobile: com.example.demopod://redirect + // Desktop: http://localhost:4400/redirect + // (can use any port as long as it matches with the one in your id document) + redirectUri: 'http://localhost:4400/redirect', + postLogoutRedirectUri: 'http://localhost:4400/redirect', + autoLogin: true, child: appScaffold, ), ); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index b8083263..350340c9 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -20,6 +20,13 @@ dependencies: universal_io: ^2.3.1 window_manager: ^0.5.1 +dependency_overrides: + solidpod: + # path: ../../solidpod + git: + url: https://github.com/anusii/solidui.git + ref: av/40_migrate_oidc_implementation + flutter: uses-material-design: true assets: diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart new file mode 100644 index 00000000..5fdc848c --- /dev/null +++ b/example/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +// import 'package:flutter/material.dart'; +// import 'package:flutter_test/flutter_test.dart'; + +// import 'package:demopod/main.dart'; + +// void main() { +// testWidgets('Counter increments smoke test', (WidgetTester tester) async { +// // Build our app and trigger a frame. +// await tester.pumpWidget(const MyApp()); + +// // Verify that our counter starts at 0. +// expect(find.text('0'), findsOneWidget); +// expect(find.text('1'), findsNothing); + +// // Tap the '+' icon and trigger a frame. +// await tester.tap(find.byIcon(Icons.add)); +// await tester.pump(); + +// // Verify that our counter has incremented. +// expect(find.text('0'), findsNothing); +// expect(find.text('1'), findsOneWidget); +// }); +// } diff --git a/lib/src/handlers/solid_auth_handler.dart b/lib/src/handlers/solid_auth_handler.dart index a9ff6acd..1402b1d3 100644 --- a/lib/src/handlers/solid_auth_handler.dart +++ b/lib/src/handlers/solid_auth_handler.dart @@ -86,6 +86,18 @@ class SolidAuthConfig { final VoidCallback? onLogout; + /// URL of the app's client profile JSON-LD document. Required parameter + + final String? clientId; + + /// Custom URL scheme for the OAuth to redirect to after authentication. + + final String? redirectUri; + + /// Optional redirect URI for logout. + + final String? postLogoutRedirectUri; + const SolidAuthConfig({ this.returnTo, this.loginPageBuilder, @@ -98,6 +110,9 @@ class SolidAuthConfig { this.loginSuccessWidget, this.onSecurityKeyReset, this.onLogout, + this.clientId, + this.redirectUri, + this.postLogoutRedirectUri, }); } @@ -258,6 +273,9 @@ class SolidAuthHandler { themeConfig: _cachedThemeConfig, snackbarConfig: _cachedSnackbarConfig, required: _cachedRequired, + clientId: _config!.clientId!, + redirectUri: _config!.redirectUri!, + postLogoutRedirectUri: _config!.postLogoutRedirectUri!, ); } @@ -273,6 +291,9 @@ class SolidAuthHandler { appLink: _config?.appLink, loginSuccessWidget: _config?.loginSuccessWidget, navigateToRootOnSuccess: _config?.loginSuccessWidget == null, + clientId: _config!.clientId!, + redirectUri: _config!.redirectUri!, + postLogoutRedirectUri: _config!.postLogoutRedirectUri!, ); } diff --git a/lib/src/widgets/solid_default_login.dart b/lib/src/widgets/solid_default_login.dart index d884be24..f8b55930 100644 --- a/lib/src/widgets/solid_default_login.dart +++ b/lib/src/widgets/solid_default_login.dart @@ -113,6 +113,18 @@ class SolidDefaultLogin extends StatelessWidget { final bool required; + /// URL of the app's client profile JSON-LD document. Required parameter + + final String clientId; + + /// Custom URL scheme for the OAuth to redirect to after authentication. + + final String redirectUri; + + /// Optional redirect URI for logout. + + final String? postLogoutRedirectUri; + const SolidDefaultLogin({ super.key, required this.appTitle, @@ -131,6 +143,9 @@ class SolidDefaultLogin extends StatelessWidget { this.themeConfig, this.snackbarConfig, this.required = false, + required this.clientId, + required this.redirectUri, + this.postLogoutRedirectUri, }); @override @@ -168,6 +183,9 @@ class SolidDefaultLogin extends StatelessWidget { changeKeyButtonStyle ?? const ChangeKeyButtonStyle(), themeConfig: themeConfig ?? const SolidLoginTheme(), snackbarConfig: snackbarConfig ?? const SnackbarConfig(), + clientId: clientId, + redirectUri: redirectUri, + postLogoutRedirectUri: postLogoutRedirectUri, child: successWidget, ), ); diff --git a/lib/src/widgets/solid_login.dart b/lib/src/widgets/solid_login.dart index e7c2286d..1982a059 100644 --- a/lib/src/widgets/solid_login.dart +++ b/lib/src/widgets/solid_login.dart @@ -40,7 +40,8 @@ import 'package:solidpod/solidpod.dart' generateDefaultFolders, generateDefaultFiles, generateCustomFolders, - setAppDirName; + setAppDirName, + tryRestoreSession; import 'package:solidui/src/constants/solid_config.dart'; import 'package:solidui/src/handlers/solid_auth_handler.dart'; @@ -81,6 +82,10 @@ class SolidLogin extends StatefulWidget { this.themeConfig = const SolidLoginTheme(), this.snackbarConfig = const SnackbarConfig(), this.customFolderPathList = const [], + required this.clientId, + required this.redirectUri, + this.postLogoutRedirectUri, + this.autoLogin = false, super.key, }); @@ -144,6 +149,26 @@ class SolidLogin extends StatefulWidget { final List customFolderPathList; + /// URL of the app's client profile JSON-LD document. Required parameter + + final String clientId; + + /// Custom URL scheme for the OAuth to redirect to after authentication. + + final String redirectUri; + + /// Optional redirect URI for logout. + + final String? postLogoutRedirectUri; + + /// When true, automatically restores a saved session on startup and navigates + /// directly to [child] without showing the login page. + /// + /// Falls back to showing the login page if no valid session is found or if + /// the user has opted out of "Stay signed in". + + final bool autoLogin; + @override State createState() => _SolidLoginState(); } @@ -184,6 +209,10 @@ class _SolidLoginState extends State with WidgetsBindingObserver { bool _assetsResolved = false; + /// Whether an auto-login check is in progress (shows a loading screen). + + bool _checkingAutoLogin = false; + /// Whether the user wishes to persist the login session across app restarts. bool _staySignedIn = true; @@ -217,6 +246,13 @@ 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 (widget.autoLogin) { + WidgetsBinding.instance.addPostFrameCallback((_) => _checkAutoLogin()); + } + // dc 20251022: please explain why calling an async without await. _initPackageInfo(); @@ -271,6 +307,32 @@ class _SolidLoginState extends State with WidgetsBindingObserver { if (mounted) setState(() => _staySignedIn = value); } + /// Attempts silent session restoration. On success navigates directly to + /// [widget.child]; on failure shows the login page as normal. + + Future _checkAutoLogin() async { + if (!mounted) return; + + // Honour the "Stay signed in" opt-out — if disabled, skip auto-login. + final staySignedIn = await SolidLoginAuthHandler.getStaySignedIn(); + if (!staySignedIn || !mounted) return; + + if (mounted) setState(() => _checkingAutoLogin = true); + + final session = await tryRestoreSession(); + + if (!mounted) return; + if (session == null) { + setState(() => _checkingAutoLogin = false); + return; + } + + // Session restored — navigate directly to the child widget. + await Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => widget.child), + ); + } + @override void dispose() { WidgetsBinding.instance.removeObserver(this); @@ -366,9 +428,10 @@ class _SolidLoginState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { - // Show a loading indicator whilst assets are being resolved. + // Show a loading indicator whilst assets are being resolved or an + // auto-login check is in progress. - if (!_assetsResolved) { + if (!_assetsResolved || _checkingAutoLogin) { return const Scaffold(body: Center(child: CircularProgressIndicator())); } @@ -408,6 +471,9 @@ class _SolidLoginState extends State with WidgetsBindingObserver { updateDialogCanceledState: updateState, showSnackbar: _showSnackbar, staySignedIn: _staySignedIn, + clientId: widget.clientId, + redirectUri: widget.redirectUri, + postLogoutRedirectUri: widget.postLogoutRedirectUri, ); } diff --git a/lib/src/widgets/solid_login_actions.dart b/lib/src/widgets/solid_login_actions.dart index a70acd99..dd796755 100644 --- a/lib/src/widgets/solid_login_actions.dart +++ b/lib/src/widgets/solid_login_actions.dart @@ -85,6 +85,9 @@ class SolidLoginActions { required VoidCallback updateDialogCanceledState, required LoginSnackbar showSnackbar, required bool staySignedIn, + required final String clientId, + required final String redirectUri, + final String? postLogoutRedirectUri, }) async { // When the user has opted out of staying signed in, discard any existing // cached session immediately so browser authentication is always @@ -132,6 +135,9 @@ class SolidLoginActions { updateDialogCanceledState: updateDialogCanceledState, showSnackbar: showSnackbar, staySignedIn: staySignedIn, + clientId: clientId, + redirectUri: redirectUri, + postLogoutRedirectUri: postLogoutRedirectUri, ); } diff --git a/lib/src/widgets/solid_login_auth_handler.dart b/lib/src/widgets/solid_login_auth_handler.dart index 11e7c208..1657e8e2 100644 --- a/lib/src/widgets/solid_login_auth_handler.dart +++ b/lib/src/widgets/solid_login_auth_handler.dart @@ -250,11 +250,14 @@ class SolidLoginAuthHandler { required List defaultFolders, required Map defaultFiles, required dynamic originalLoginWidget, + required final String clientId, + required final String redirectUri, required Widget childWidget, required ValueGetter isDialogCanceled, required VoidCallback updateDialogCanceledState, required Function(String message, {Duration? duration, bool showAction}) showSnackbar, + final String? postLogoutRedirectUri, bool staySignedIn = true, }) async { // Method to show busy animation requiring BuildContext. @@ -305,7 +308,13 @@ class SolidLoginAuthHandler { List? authResult; try { - authResult = await solidAuthenticate(podServer, context); + authResult = await solidAuthenticate( + podServer, + context, + clientId: clientId, + redirectUri: redirectUri, + postLogoutRedirectUri: postLogoutRedirectUri, + ); } on Object catch (e) { // Check whether auth data was persisted before the failure (i.e. POD // not initialised) vs a genuine server/network error. diff --git a/lib/src/widgets/solid_popup_login.dart b/lib/src/widgets/solid_popup_login.dart index f2f0a498..eb35327e 100644 --- a/lib/src/widgets/solid_popup_login.dart +++ b/lib/src/widgets/solid_popup_login.dart @@ -141,7 +141,8 @@ class _SolidPopupLoginState extends State { Future _loginAndInitPods(String webId, BuildContext context) async { try { - await solidAuthenticate(webId, context); + await solidAuthenticate(webId, context, + clientId: '', redirectUri: '', postLogoutRedirectUri: ''); // Persist the WebID/server URL so the re-login dialog can prefill it // next time the user is logged out. Prefer the canonical WebID diff --git a/pubspec.yaml b/pubspec.yaml index 2540fe77..562df527 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,13 @@ dependencies: url_launcher: ^6.3.2 version_widget: ^1.0.9 +dependency_overrides: + solidpod: + path: ../solidpod + # git: + # url: https://github.com/anusii/solidpod.git + # ref: av/40_migrate_oidc_implementation + dev_dependencies: flutter_lints: ^6.0.0 window_manager: ^0.5.1