diff --git a/.gitignore b/.gitignore index f4aceb6..a85654d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ ARCHIVE ignore pubspec.yaml.* -!templates/solidpod/pubspec.yaml.tmpl +!templates/solidui/pubspec.yaml.tmpl qtest_*.txt diff --git a/.pubignore b/.pubignore index a7371d3..2d6d305 100644 --- a/.pubignore +++ b/.pubignore @@ -15,6 +15,8 @@ ARCHIVE ignore pubspec.yaml.* +!templates/solidui/pubspec.yaml.tmpl + .flutter-plugins* qtest_*.txt diff --git a/README.md b/README.md index 5c5c52c..98622e4 100644 --- a/README.md +++ b/README.md @@ -181,13 +181,149 @@ SolidUI requires the following dependencies: ## Quick Start to Create an App -To create a new Solid-based app using `solidui` named `myapp` and -published by `example.com` begin with: +`solidui` ships with an app template — a ready-to-run Pod file browser, +complete with a navigation rail and a status bar, built on `solidui`. It is the +practical equivalent of a `flutter create --template=solidui`, which stock +Flutter cannot offer because the `--template` flag only accepts a fixed set of +built-in types. We provide a small generator instead. + +The recommended way is to activate the generator once and then run it from any +directory: + +```bash +flutter pub global activate solidui +solidui create my_pod_app +``` + +Alternatively, `dart run solidui:create` works **only from within a package +that already depends on `solidui`** (for example a clone of the `solidui` +repository), because `dart run` must resolve the `solidui:create` executable +through that project's `pubspec.yaml`: + +```bash +dart run solidui:create my_pod_app +``` + +Running `dart run solidui:create` from an unrelated directory fails with +`Found no pubspec.yaml file in or parent directories` — use the global +activation above instead. + +### Running from a local checkout (before publishing) + +If you are working on a branch of `solidui` that is not yet published to +pub.dev, you can still generate an app from any directory without publishing. +Replace `/path/to/solidui` below with the path to your local checkout. + +- Run the generator script directly. This always uses your current working tree + — both `bin/create.dart` and the template files — so it picks up your edits + on every run: + + ```bash + dart run /path/to/solidui/bin/create.dart my_pod_app + ``` + +- Or activate the local checkout and use the short `solidui create` command + anywhere: + + ```bash + flutter pub global activate --source path /path/to/solidui + solidui create my_pod_app + ``` + + Note that this takes a snapshot of `bin/create.dart`, so re-run the + `activate` command after editing the generator itself; edits to the template + files are picked up without re-activating. + +For Windows users, command `solidui` can be added to the system by the +following steps: + +- Run **PowerShell** +- Run the following commands: + + ```shell + $pubCacheBin = "$env:LOCALAPPDATA\Pub\Cache\bin" + $userPath = [Environment]::GetEnvironmentVariable("Path", "User") + + if ($userPath -notlike "*$pubCacheBin*") { + [Environment]::SetEnvironmentVariable("Path", "$userPath;$pubCacheBin", "User") + } + ``` + +- Close **Powershell** +- Open **Command Prompter** or **PowerShell** and use `solidui` command. + +The generator runs `flutter create` to lay down the platform folders, overlays +the template (substituting your app name), and runs `flutter pub get`. Useful +options: + +| Option | Description | +|-----------------------|------------------------------------------------------| +| `--org ` | Reverse-domain org id (default: `com.example`). | +| `--title ` | Window title shown by the app. | +| `--description `| `pubspec` description. | +| `-o, --output ` | Output directory (default: the app name). | +| `--no-flutter-create` | Render template only; skip platform folders. | +| `--no-pub-get` | Skip the final `flutter pub get`. | + +The generated app starts at a `SolidLogin` screen and, once signed in, shows a +`SolidScaffold` with a home page, an app-files browser and a whole-POD browser. + +### Enabling login (Solid-OIDC client registration) + +Before login will work you must publish a Client Identifier Document for the +app. The generator writes a ready-to-deploy copy of it, together with the web +redirect helper, into the generated project's `solid/` folder: + +- `solid/client-profile.jsonld` — the Solid-OIDC Client Identifier Document. Its + `redirect_uris` are generated to match, byte for byte, the `redirectUris` + passed to `SolidLogin` in `lib/app.dart`. +- `solid/redirect.html` — the web and post-logout redirect helper used by the + `oidc` package. + +Two points are worth understanding: + +- `client-profile.jsonld` is **not** a file the app creates on your POD, and it + cannot be — the app is not yet authenticated at login time. It must already be + hosted, and be publicly readable, at the URL given as the `clientId`. During + login the identity provider fetches that URL to learn which `redirect_uris` + are permitted; if it is missing (HTTP 404) the provider refuses to hand + control back to the app after the consent screen, and login fails with an + `ASWebAuthenticationSession Code=1` (cancelled) error. +- The POD data folders (for example `/data` and `/sharing`) are + created by solidpod's `generateDefaultFolders()` **after** a successful login. + If they have not appeared, it is because login has not completed — that is a + symptom of the missing client profile, not the cause. + +To enable login, publish both files at the location your `clientId` points to. +If you maintain the Solid server — for example the Australian Solid Community +(`solidcommunity.au`) — deploy them alongside the other apps exactly as +`filepod` does: + +```console +https://solidcommunity.au/apps/my_pod_app/client-profile.jsonld +https://solidcommunity.au/apps/my_pod_app/redirect.html +``` + +Then confirm the document is reachable (a public `200`, requiring no +authentication): ```bash -flutter create --template solidui --domain com.example myapp +curl -I https://solidcommunity.au/apps/my_pod_app/client-profile.jsonld ``` +Once it returns `200`, run `flutter run` and the login redirect will complete +(`filepod`'s own document returns `200`, which is why it can sign in). +Otherwise, host the two files at any public URL you control and update the +`clientId` — and the matching `redirect.html` entry in `redirectUris` — in +`lib/app.dart` accordingly. Note that only the custom redirect **scheme** +(`.://redirect`, e.g. `com.example.mypodapp`) +drops the underscores from the project name, because a URI scheme may not +contain them; every other identifier keeps the project name as-is. + +After generating, also review the remaining placeholders — the `clientId`, +`redirectUris` and `link` in `lib/app.dart`, and the constants in +`lib/constants/app.dart` — and update them for your own deployment. + A demonstrator example application (DemoPod) is available in the [example](example/) folder of this repository. DemoPod showcases the suite of functionality provided by `solidpod` and `solidui`, diff --git a/bin/create.dart b/bin/create.dart new file mode 100644 index 0000000..4c6f32a --- /dev/null +++ b/bin/create.dart @@ -0,0 +1,601 @@ +/// Solidui:create - scaffold a new Solid Pod file-browser app. +/// +/// 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 'dart:io'; +import 'dart:isolate'; + +// File extensions copied verbatim (never treated as text for token +// substitution). + +const _binaryExtensions = { + '.png', + '.jpg', + '.jpeg', + '.gif', + '.ico', + '.webp', +}; + +// Operating-system and editor cruft that must never be copied into a generated +// project (and would otherwise break the text decoder). + +const _junkFiles = { + '.DS_Store', + 'Thumbs.db', +}; + +// Dart reserved words that cannot be used as a package name. + +const _reservedWords = { + 'abstract', 'as', 'assert', 'async', 'await', 'break', 'case', 'catch', + 'class', 'const', 'continue', 'covariant', 'default', 'deferred', 'do', + 'dynamic', 'else', 'enum', 'export', 'extends', 'extension', 'external', + 'factory', 'false', 'final', 'finally', 'for', 'function', 'get', 'hide', + 'if', 'implements', 'import', 'in', 'interface', 'is', 'library', 'mixin', + 'new', 'null', 'on', 'operator', 'part', 'rethrow', 'return', 'set', 'show', + 'static', 'super', 'switch', 'sync', 'this', 'throw', 'true', 'try', + 'typedef', 'var', 'void', 'while', 'with', 'yield', + // Names that would clash with the Flutter toolchain. + 'flutter', 'test', +}; + +Future main(List arguments) async { + final args = _Args.parse(arguments); + + if (args.help) { + _printUsage(stdout); + return; + } + + final projectName = args.projectName; + if (projectName == null) { + stderr.writeln('Error: missing .\n'); + _printUsage(stderr); + exitCode = 64; // EX_USAGE + return; + } + + final nameError = _validateProjectName(projectName); + if (nameError != null) { + stderr.writeln('Error: $nameError'); + exitCode = 64; + return; + } + + // Derive the human-facing names from the package name unless overridden. + + final appName = _pascalCase(projectName); + final appTitle = args.title ?? '$appName - File Browser for Solid Pods'; + final appDescription = args.description ?? + '$appName - manage files on your personal online data store (POD).'; + final orgName = args.org; + + // The project name is used as-is almost everywhere (package name, imports, + // applicationId, POD folder, and the clientId / redirect URL paths, which all + // permit underscores). The ONE exception is the custom redirect URI scheme: a + // URI scheme may not contain underscores (RFC 3986 allows only letters, + // digits and "+", "-", "."), and an Android intent-filter scheme must be + // lower case. So we derive a scheme name with the underscores stripped, and + // use it only inside the `org.scheme://` redirect. + + final schemeName = projectName.replaceAll('_', ''); + final outputDir = Directory(args.output ?? projectName); + + final templateDir = await _resolveTemplateDir(); + if (templateDir == null || !templateDir.existsSync()) { + stderr.writeln( + 'Error: could not locate the solidui template directory.\n' + 'Expected it alongside the solidui package (templates/solidui/).', + ); + exitCode = 70; // EX_SOFTWARE + return; + } + + final tokens = { + '{{projectName}}': projectName, + '{{appName}}': appName, + '{{appTitle}}': appTitle, + '{{appDescription}}': appDescription, + '{{orgName}}': orgName, + '{{schemeName}}': schemeName, + }; + + stdout.writeln('Creating Solid Pod app "$appName" in ${outputDir.path}/ ...'); + + // Step 1: let `flutter create` lay down the platform folders and tooling + // (android/, ios/, etc.) unless the caller opted out. We pass the project + // name and org so the generated metadata matches our overlaid pubspec. + + if (args.runFlutterCreate) { + final created = await _runFlutterCreate( + projectName: projectName, + org: orgName, + output: outputDir, + ); + if (!created) { + exitCode = 70; + return; + } + } else { + outputDir.createSync(recursive: true); + } + + // Step 2: overlay the template, substituting tokens as we go. This overwrites + // the default main.dart and pubspec.yaml that flutter create produced. + + stdout.writeln('Applying the solidui template ...'); + _renderTemplate( + source: templateDir, + target: outputDir, + tokens: tokens, + ); + + // Step 3: drop the default counter widget test, which references the + // scaffolding flutter create generated rather than our app. + + final defaultTest = File('${outputDir.path}/test/widget_test.dart'); + if (defaultTest.existsSync()) { + defaultTest.deleteSync(); + } + + // Step 4: wire up the OIDC redirect on the platforms that flutter create does + // not configure (Android manifest placeholder and the iOS URL scheme). The + // macOS entitlements are supplied by the template overlay above. + + if (args.runFlutterCreate) { + _patchAuthRedirect(outputDir, scheme: '$orgName.$schemeName'); + } + + // Step 5: resolve dependencies now that the pubspec lists solidui. + + if (args.runPubGet && args.runFlutterCreate) { + stdout.writeln('Running flutter pub get ...'); + await _runProcess('flutter', ['pub', 'get'], outputDir.path); + } + + _printNextSteps( + stdout, + outputDir.path, + runFlutterCreate: args.runFlutterCreate, + ); +} + +// ── Template rendering ───────────────────────────────────────────────────── + +void _renderTemplate({ + required Directory source, + required Directory target, + required Map tokens, +}) { + final sourcePath = source.path; + for (final entity in source.listSync(recursive: true)) { + if (entity is! File) continue; + + final name = entity.uri.pathSegments.last; + + // Skip operating-system and editor junk that may sit alongside the template + // (e.g. macOS .DS_Store), so it never lands in the generated project nor + // trips up the text decoder below. + + if (_junkFiles.contains(name)) continue; + + // Path of the file relative to the template root. + + var relative = entity.path.substring(sourcePath.length); + relative = relative.replaceFirst(RegExp(r'^[\\/]+'), ''); + + // Strip the .tmpl marker and substitute any tokens that appear in the path + // itself (so template authors can parameterise file names too). + + if (relative.endsWith('.tmpl')) { + relative = relative.substring(0, relative.length - '.tmpl'.length); + } + relative = _substitute(relative, tokens); + + final destination = File('${target.path}/$relative'); + destination.parent.createSync(recursive: true); + + // Known-binary files are copied verbatim. Anything else we attempt to read + // as text for token substitution, falling back to a byte copy if it turns + // out not to be valid UTF-8 (so an unexpected binary never crashes us). + + if (_isBinary(relative)) { + destination.writeAsBytesSync(entity.readAsBytesSync()); + continue; + } + try { + final rendered = _substitute(entity.readAsStringSync(), tokens); + destination.writeAsStringSync(rendered); + } on FileSystemException { + destination.writeAsBytesSync(entity.readAsBytesSync()); + } + } +} + +String _substitute(String input, Map tokens) { + var output = input; + tokens.forEach((token, value) { + output = output.replaceAll(token, value); + }); + return output; +} + +bool _isBinary(String path) { + final dot = path.lastIndexOf('.'); + if (dot < 0) return false; + return _binaryExtensions.contains(path.substring(dot).toLowerCase()); +} + +// ── OIDC redirect wiring ─────────────────────────────────────────────────── + +// flutter create does not register the custom redirect scheme that the OIDC +// login needs. We add the Android manifest placeholder and the iOS URL scheme +// here; the macOS network-client/keychain entitlements come from the template. + +void _patchAuthRedirect(Directory output, {required String scheme}) { + _patchAndroidRedirectScheme(output, scheme); + _patchIosUrlScheme(output, scheme); +} + +void _patchAndroidRedirectScheme(Directory output, String scheme) { + // flutter_appauth reads `appAuthRedirectScheme` from the manifest placeholders + // declared in the app-level Gradle file (Kotlin or Groovy DSL). + + final placeholder = 'appAuthRedirectScheme'; + + final kts = File('${output.path}/android/app/build.gradle.kts'); + if (kts.existsSync()) { + final content = kts.readAsStringSync(); + if (content.contains(placeholder)) return; + final patched = content.replaceFirstMapped( + RegExp(r'versionName = flutter\.versionName\n'), + (m) => '${m[0]} manifestPlaceholders.putAll(mapOf(\n' + ' "$placeholder" to "$scheme",\n' + ' ))\n', + ); + if (patched != content) { + kts.writeAsStringSync(patched); + } else { + stderr.writeln( + 'Warning: could not add $placeholder to build.gradle.kts; add ' + 'manifestPlaceholders["$placeholder"] = "$scheme" to defaultConfig ' + 'manually.', + ); + } + return; + } + + final groovy = File('${output.path}/android/app/build.gradle'); + if (groovy.existsSync()) { + final content = groovy.readAsStringSync(); + if (content.contains(placeholder)) return; + final patched = content.replaceFirstMapped( + RegExp(r'versionName flutterVersionName\n'), + (m) => + '${m[0]} manifestPlaceholders = [$placeholder: "$scheme"]\n', + ); + if (patched != content) { + groovy.writeAsStringSync(patched); + } else { + stderr.writeln( + 'Warning: could not add $placeholder to build.gradle; add ' + 'manifestPlaceholders = [$placeholder: "$scheme"] to defaultConfig ' + 'manually.', + ); + } + } +} + +void _patchIosUrlScheme(Directory output, String scheme) { + final plist = File('${output.path}/ios/Runner/Info.plist'); + if (!plist.existsSync()) return; + + final content = plist.readAsStringSync(); + if (content.contains('CFBundleURLTypes')) return; + + // Register the custom scheme so iOS can return to the app after login. + + final block = ''' + CFBundleURLTypes + + + CFBundleURLSchemes + + $scheme + + + +'''; + + final patched = content.replaceFirst( + RegExp(r'\n\n'), + '\n$block\n', + ); + if (patched != content) { + plist.writeAsStringSync(patched); + } else { + stderr.writeln( + 'Warning: could not add CFBundleURLTypes to ios/Runner/Info.plist; add ' + 'the "$scheme" URL scheme manually.', + ); + } +} + +// ── Template location ────────────────────────────────────────────────────── + +Future _resolveTemplateDir() async { + // Preferred: resolve through the package config so this works whether invoked + // as `dart run solidui:create` or after a global activation. + + try { + final libUri = await Isolate.resolvePackageUri( + Uri.parse('package:solidui/solidui.dart'), + ); + if (libUri != null) { + final dir = Directory.fromUri(libUri.resolve('../templates/solidui/')); + if (dir.existsSync()) return dir; + } + } catch (_) { + // Fall through to the script-relative lookup below. + } + + // Fallback: relative to this script (bin/create.dart -> ../templates/...). + + try { + final dir = Directory.fromUri( + Platform.script.resolve('../templates/solidui/'), + ); + if (dir.existsSync()) return dir; + } catch (_) { + // Ignored - caller handles the null result. + } + + return null; +} + +// ── Process helpers ──────────────────────────────────────────────────────── + +Future _runFlutterCreate({ + required String projectName, + required String org, + required Directory output, +}) async { + stdout.writeln('Running flutter create ...'); + return _runProcess( + 'flutter', + [ + 'create', + '--project-name', + projectName, + '--org', + org, + '--no-pub', + output.path, + ], + null, + ); +} + +Future _runProcess( + String executable, + List args, + String? workingDirectory, +) async { + try { + final process = await Process.start( + executable, + args, + workingDirectory: workingDirectory, + mode: ProcessStartMode.inheritStdio, + runInShell: true, + ); + final code = await process.exitCode; + if (code != 0) { + stderr + .writeln('Error: `$executable ${args.join(' ')}` exited with $code.'); + return false; + } + return true; + } on ProcessException catch (e) { + stderr.writeln( + 'Error: could not run `$executable`. Is it installed and on your PATH?\n' + ' ${e.message}', + ); + return false; + } +} + +// ── Naming helpers ───────────────────────────────────────────────────────── + +String? _validateProjectName(String name) { + if (!RegExp(r'^[a-z][a-z0-9_]*$').hasMatch(name)) { + return '"$name" is not a valid package name. Use lowercase letters, ' + 'digits and underscores, starting with a letter ' + '(e.g. my_pod_app).'; + } + if (_reservedWords.contains(name)) { + return '"$name" is a reserved word and cannot be used as a package name.'; + } + return null; +} + +String _pascalCase(String snake) { + return snake + .split('_') + .where((part) => part.isNotEmpty) + .map((part) => part[0].toUpperCase() + part.substring(1)) + .join(); +} + +// ── Argument parsing ─────────────────────────────────────────────────────── + +class _Args { + _Args({ + required this.projectName, + required this.org, + required this.title, + required this.description, + required this.output, + required this.runFlutterCreate, + required this.runPubGet, + required this.help, + }); + + final String? projectName; + final String org; + final String? title; + final String? description; + final String? output; + final bool runFlutterCreate; + final bool runPubGet; + final bool help; + + static _Args parse(List arguments) { + String? projectName; + var org = 'com.example'; + String? title; + String? description; + String? output; + var runFlutterCreate = true; + var runPubGet = true; + var help = false; + + String valueFor(String arg, String? inlineValue, Iterator it) { + if (inlineValue != null) return inlineValue; + if (it.moveNext()) return it.current; + stderr.writeln('Error: option "$arg" expects a value.'); + exit(64); + } + + final it = arguments.iterator; + while (it.moveNext()) { + final arg = it.current; + + // Allow an optional leading `create` verb so both `solidui create app` + // and `solidui app` work after a global activation. + + if (arg == 'create' && projectName == null) continue; + + if (arg == '-h' || arg == '--help') { + help = true; + continue; + } + if (arg == '--no-flutter-create') { + runFlutterCreate = false; + continue; + } + if (arg == '--no-pub-get') { + runPubGet = false; + continue; + } + + String? key = arg; + String? inline; + final eq = arg.indexOf('='); + if (arg.startsWith('--') && eq != -1) { + key = arg.substring(0, eq); + inline = arg.substring(eq + 1); + } + + switch (key) { + case '--org': + org = valueFor(arg, inline, it); + case '--title': + title = valueFor(arg, inline, it); + case '--description': + description = valueFor(arg, inline, it); + case '--output': + case '-o': + output = valueFor(arg, inline, it); + default: + if (arg.startsWith('-')) { + stderr.writeln('Error: unknown option "$arg".'); + exit(64); + } + projectName ??= arg; + } + } + + return _Args( + projectName: projectName, + org: org, + title: title, + description: description, + output: output, + runFlutterCreate: runFlutterCreate, + runPubGet: runPubGet, + help: help, + ); + } +} + +void _printUsage(IOSink out) { + out.writeln(''' +Scaffold a new Solid Pod file-browser app from the solidui template. + +Usage: + dart run solidui:create [options] + +Options: + --org Reverse-domain organisation id (default: com.example). + --title Window title (default: " - File Browser for + Solid Pods"). + --description pubspec description. + -o, --output Output directory (default: ). + --no-flutter-create Only render the template; skip running flutter create + (no platform folders are generated). + --no-pub-get Skip the final flutter pub get. + -h, --help Show this help. + +Example: + dart run solidui:create my_pod_app --org au.org.example +'''); +} + +void _printNextSteps( + IOSink out, + String path, { + required bool runFlutterCreate, +}) { + out.writeln(''' + +Done! Your Solid Pod app is ready in $path/ + +Next steps: + cd $path${runFlutterCreate ? '' : '\n flutter create --project-name . # generate platform folders\n flutter pub get'} + flutter run + +Then update the Solid app registration (clientId, redirectUris, link) in +lib/app.dart and the constants in lib/constants/app.dart for your deployment. + +On macOS/iOS, enable signing once in Xcode (Signing & Capabilities -> Team) +so the keychain-backed login can build. See the generated README for details. +'''); +} diff --git a/lib/src/widgets/solid_login.dart b/lib/src/widgets/solid_login.dart index 540d93d..5d4fb88 100644 --- a/lib/src/widgets/solid_login.dart +++ b/lib/src/widgets/solid_login.dart @@ -34,7 +34,6 @@ library; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:markdown_tooltip/markdown_tooltip.dart'; import 'package:solidpod/solidpod.dart' show cancelSolidAuthenticate, @@ -576,53 +575,20 @@ class _SolidLoginState extends State with WidgetsBindingObserver { // "Stay signed in" checkbox. - final staySignedInCheckbox = Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - MarkdownTooltip( - message: - '**Stay signed in**\n\nWhen ticked, your login session will be ' - 'cached so you can skip the browser login next time. ' - 'Untick to require a fresh login on every launch.', - child: SizedBox( - width: 24, - height: 24, - child: Checkbox( - value: _staySignedIn, - onChanged: (value) { - final newValue = value ?? true; - setState(() => _staySignedIn = newValue); - SolidLoginAuthHandler.setStaySignedIn(newValue); - }, - ), - ), - ), - const SizedBox(width: 8), - GestureDetector( - onTap: () { - final newValue = !_staySignedIn; - setState(() => _staySignedIn = newValue); - SolidLoginAuthHandler.setStaySignedIn(newValue); - }, - child: Text( - 'Stay signed in', - style: TextStyle(color: currentTheme.textColor, fontSize: 14), - ), - ), - ], + final staySignedInCheckbox = + SolidLoginBuildHelper.buildStaySignedInCheckbox( + value: _staySignedIn, + onChanged: (newValue) { + setState(() => _staySignedIn = newValue); + SolidLoginAuthHandler.setStaySignedIn(newValue); + }, + textColor: currentTheme.textColor, ); - final tryAnotherAccountButton = TextButton( + final tryAnotherAccountButton = + SolidLoginBuildHelper.buildTryAnotherAccountButton( onPressed: performTryAnotherAccount, - child: Text( - 'Try another WebID', - style: TextStyle( - color: currentTheme.textColor.withValues(alpha: 0.7), - fontSize: 14, - decoration: TextDecoration.underline, - ), - ), + textColor: currentTheme.textColor, ); // Build the login panel content. diff --git a/lib/src/widgets/solid_login_build_helper.dart b/lib/src/widgets/solid_login_build_helper.dart index 21ae16a..4b449fc 100644 --- a/lib/src/widgets/solid_login_build_helper.dart +++ b/lib/src/widgets/solid_login_build_helper.dart @@ -32,6 +32,8 @@ import 'dart:async' show unawaited; import 'package:flutter/material.dart'; +import 'package:markdown_tooltip/markdown_tooltip.dart'; + import 'package:solidui/src/constants/solid_config.dart'; import 'package:solidui/src/widgets/create_account_dialog.dart'; import 'package:solidui/src/widgets/solid_login_buttons.dart'; @@ -124,6 +126,65 @@ class SolidLoginBuildHelper { ); } + /// Builds the "Stay signed in" checkbox row. + /// + /// [onChanged] receives the new value when either the checkbox or its label + /// is tapped, leaving persistence and state updates to the caller. + + static Widget buildStaySignedInCheckbox({ + required bool value, + required ValueChanged onChanged, + required Color textColor, + }) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + MarkdownTooltip( + message: + '**Stay signed in**\n\nWhen ticked, your login session will be ' + 'cached so you can skip the browser login next time. ' + 'Untick to require a fresh login on every launch.', + child: SizedBox( + width: 24, + height: 24, + child: Checkbox( + value: value, + onChanged: (v) => onChanged(v ?? true), + ), + ), + ), + const SizedBox(width: 8), + GestureDetector( + onTap: () => onChanged(!value), + child: Text( + 'Stay signed in', + style: TextStyle(color: textColor, fontSize: 14), + ), + ), + ], + ); + } + + /// Builds the "Try another WebID" text button. + + static Widget buildTryAnotherAccountButton({ + required VoidCallback onPressed, + required Color textColor, + }) { + return TextButton( + onPressed: onPressed, + child: Text( + 'Try another WebID', + style: TextStyle( + color: textColor.withValues(alpha: 0.7), + fontSize: 14, + decoration: TextDecoration.underline, + ), + ), + ); + } + /// Builds the login body scaffold. static Widget buildScaffold({ diff --git a/pubspec.yaml b/pubspec.yaml index 8af51ee..9bd171f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,6 +3,18 @@ description: 'A UI library for building Solid applications with Flutter.' version: 1.0.14 homepage: https://github.com/anusii/solidui +# Scaffold a new Solid Pod file-browser app (a pod browser, with navigation +# rail and status bar, built on solidui) from the bundled template: +# +# dart run solidui:create my_pod_app +# +# Or, after `flutter pub global activate solidui`: +# +# solidui create my_pod_app + +executables: + solidui: create + environment: sdk: '>=3.2.3 <4.0.0' flutter: '>=3.44.0' diff --git a/templates/solidui/README.md.tmpl b/templates/solidui/README.md.tmpl new file mode 100644 index 0000000..47a6543 --- /dev/null +++ b/templates/solidui/README.md.tmpl @@ -0,0 +1,84 @@ +# {{appName}} + +{{appDescription}} + +This app was generated from the [`solidui`](https://pub.dev/packages/solidui) +app template — a Solid Pod file browser built with the +[`solidui`](https://pub.dev/packages/solidui) widget set. It comes ready with: + +- a [`SolidLogin`] screen that connects to the user's data vault on their + chosen Solid server; +- a [`SolidScaffold`] with a navigation rail (collapsing to a drawer on narrow + screens) and a status bar showing the server, login and security-key state; +- a [`SolidFile`] browser for both the app's own folder and the whole POD; +- theme switching, an About dialog and an Invite Others action. + +## Getting started + +```bash +flutter pub get +flutter run +``` + +## Next steps + +A few values were filled in with placeholders when this project was generated. +Update them for your own deployment: + +- **Solid app registration** in `lib/app.dart` — the `clientId`, + `redirectUris` and `link` passed to `SolidLogin`. These identify your app to + the Solid server during login. **The `clientId` URL must actually resolve to + a Client Identifier Document (a `client-profile.jsonld`) that lists these + exact `redirectUris`.** Until you publish that document (and list your + `{{orgName}}.{{schemeName}}://redirect` scheme in it), the identity provider has + no client to validate and the login page will not appear — this is the most + common reason a freshly generated app cannot reach the login screen. See the + [Solid-OIDC client identifiers](https://solidproject.org/TR/oidc#clientids) + documentation. +- **App constants** in `lib/constants/app.dart` — the title, hosting URL and + the Invite Others message. +- **Allowed upload types** in `lib/constants/app.dart` — `appUploadConfig` + currently restricts uploads to `.md` and `.txt`; widen `allowedExtensions` + to suit your app. +- **Icons** in `assets/images/` — replace `app_icon.png` and `app_image.jpg`, + then run `dart run flutter_launcher_icons` to regenerate platform icons. + +## Login (OIDC) setup + +> **Required before login works:** publish this app's Client Identifier +> Document. The identity provider fetches your `clientId` URL to learn which +> `redirect_uris` are allowed; if it is missing the login is cancelled after the +> consent screen (`ASWebAuthenticationSession Code=1`). A ready-to-deploy +> `client-profile.jsonld` and `redirect.html` were generated in the +> [`solid/`](solid/) folder — see `solid/README.md` for where to publish them. + +The OIDC redirect is pre-wired so that login works on every platform: + +- **macOS** — `macos/Runner/*.entitlements` grant `network.client` (so the + login web session can open), keychain access (for token storage) and + user-selected file access (for uploads/downloads). +- **Android** — `android/app/build.gradle.kts` sets the `appAuthRedirectScheme` + manifest placeholder. +- **iOS** — `ios/Runner/Info.plist` registers the custom URL scheme. + +Because the macOS/iOS apps now request keychain and sandbox capabilities, you +must enable signing once: open `macos/Runner.xcworkspace` (or +`ios/Runner.xcworkspace`) in Xcode and pick your team under **Signing & +Capabilities**, or run `flutter run` with a configured Apple developer account. + +## Project layout + +``` +lib/ + main.dart Application entry point and desktop window setup. + app.dart Root widget; wraps the app in SolidLogin. + app_scaffold.dart SolidScaffold with the nav bar, status bar and menu. + home.dart Introductory home page. + constants/app.dart App-wide constants (title, upload and invite configs). + screens/ + browse_files.dart Whole-POD file browser (SolidFile at the root). +solid/ + client-profile.jsonld Solid-OIDC client document to publish (see Login setup). + redirect.html Web/post-logout redirect helper to publish alongside it. + README.md Where and how to publish the two files above. +``` diff --git a/templates/solidui/analysis_options.yaml b/templates/solidui/analysis_options.yaml new file mode 100644 index 0000000..42239ef --- /dev/null +++ b/templates/solidui/analysis_options.yaml @@ -0,0 +1,30 @@ +# Configure the analyzer for static analysis of Dart code. +# +# Add the following to pubspec.yaml: +# +# dev_dependencies: +# flutter_lints: ^6.0.0 +# +# Invoke the analyzer from the command line: +# +# flutter analyze + +# Activate recommended lints for Flutter apps, packages, and plugins +# designed to encourage good coding practices. + +include: package:flutter_lints/flutter.yaml + +linter: + rules: + avoid_print: true + prefer_single_quotes: true + require_trailing_commas: true + directives_ordering: false + prefer_const_constructors: true + +# Identify directories to ignore. + +analyzer: + exclude: + - ignore/** + - ignore/ diff --git a/templates/solidui/assets/images/app_icon.png b/templates/solidui/assets/images/app_icon.png new file mode 100644 index 0000000..7d08189 Binary files /dev/null and b/templates/solidui/assets/images/app_icon.png differ diff --git a/templates/solidui/assets/images/app_image.jpg b/templates/solidui/assets/images/app_image.jpg new file mode 100644 index 0000000..2b29209 Binary files /dev/null and b/templates/solidui/assets/images/app_image.jpg differ diff --git a/templates/solidui/lib/app.dart.tmpl b/templates/solidui/lib/app.dart.tmpl new file mode 100644 index 0000000..83fa024 --- /dev/null +++ b/templates/solidui/lib/app.dart.tmpl @@ -0,0 +1,70 @@ +/// {{appName}} - orchestrate the primary login widget. +/// +/// This file was generated from the `solidui` app template +/// (`dart run solidui:create`). Edit it freely to suit your app. + +library; + +import 'package:flutter/material.dart'; + +import 'package:solidui/solidui.dart'; + +import 'package:{{projectName}}/app_scaffold.dart'; +import 'package:{{projectName}}/constants/app.dart'; + +// This widget is the root of the application. On startup it calls upon +// [SolidLogin] to connect to the user's Pod stored within their data vault on +// their chosen Solid server. + +class App extends StatelessWidget { + const App({super.key}); + + @override + Widget build(BuildContext context) { + return SolidThemeApp( + // We can manually turn off the debug banner. It is turned off + // automatically for a `flutter --release`. + + debugShowCheckedModeBanner: false, + + title: appTitle, + + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF007AFF), + ), + useMaterial3: true, + ), + + home: SolidLogin( + title: appTitle.replaceAll(' - ', '\n'), + image: const AssetImage('assets/images/app_image.jpg'), + logo: const AssetImage('assets/images/app_icon.png'), + + // The application folder created on the user's POD. + + appDirectory: '{{projectName}}', + + // TODO Update the following Solid app registration details to point at + // your own deployment. They identify your app to the Solid server + // during login, and the clientId MUST resolve to a client profile + // document that lists these exact redirectUris (see the solid/ folder). + // See https://solidproject.org for more information. + // + // Note: the custom redirect scheme drops underscores from the project + // name ('{{orgName}}.{{schemeName}}'), because a URI scheme may not + // contain underscores. Every other identifier keeps the project name. + + link: 'https://github.com/example/{{projectName}}', + clientId: + 'https://solidcommunity.au/apps/{{projectName}}/client-profile.jsonld', + redirectUris: [ + 'https://solidcommunity.au/apps/{{projectName}}/redirect.html', + '{{orgName}}.{{schemeName}}://redirect', + 'http://localhost:4400/redirect', + ], + child: appScaffold, + ), + ); + } +} diff --git a/templates/solidui/lib/app_scaffold.dart.tmpl b/templates/solidui/lib/app_scaffold.dart.tmpl new file mode 100644 index 0000000..4911860 --- /dev/null +++ b/templates/solidui/lib/app_scaffold.dart.tmpl @@ -0,0 +1,145 @@ +/// {{appName}} - the primary application scaffold. +/// +/// This file was generated from the `solidui` app template +/// (`dart run solidui:create`). Edit it freely to suit your app. + +library; + +import 'package:flutter/material.dart'; + +import 'package:solidui/solidui.dart'; + +import 'package:{{projectName}}/constants/app.dart'; +import 'package:{{projectName}}/home.dart'; +import 'package:{{projectName}}/screens/browse_files.dart'; + +final _scaffoldController = SolidScaffoldController(); + +const appScaffold = AppScaffold(); + +class AppScaffold extends StatelessWidget { + const AppScaffold({super.key}); + + @override + Widget build(BuildContext context) { + return SolidScaffold( + controller: _scaffoldController, + hideNavRail: false, + enableProfile: true, + onLogout: (context) => SolidAuthHandler.instance.handleLogout(context), + + // The navigation menu drives the side navigation rail (and the drawer on + // narrow screens). Each entry exposes a top-level page of the app. + + menu: const [ + SolidMenuItem( + icon: Icons.home, + title: 'Home', + tooltip: ''' + + **Home** + + Tap here to return to the main page for the app. + + ''', + child: Home(title: appTitle), + ), + SolidMenuItem( + icon: Icons.folder, + title: 'App Files', + tooltip: ''' + + **Files** + + Tap here to browse the files on your POD for this app. + + ''', + child: SolidFile(uploadConfig: appUploadConfig), + ), + SolidMenuItem( + icon: Icons.storage, + title: 'All POD Files', + tooltip: ''' + + **All Files** + + Tap here to browse all folders on your POD from the root. + + ''', + child: BrowseFiles(), + ), + ], + appBar: SolidAppBarConfig( + title: appTitle.split(' - ')[0], + versionConfig: const SolidVersionConfig( + changelogUrl: 'https://github.com/example/{{projectName}}/blob/dev/' + 'CHANGELOG.md', + showUpdateButton: true, + downloadUrl: 'https://solidcommunity.au/installers/', + ), + actions: [ + SolidAppBarAction( + icon: Icons.folder, + onPressed: () => _scaffoldController.navigateToSubpage( + const SolidFile(uploadConfig: appUploadConfig), + ), + tooltip: 'Files', + ), + ], + ), + + // The status bar runs along the bottom of the window, surfacing the + // current server, login state and security key status. + + statusBar: const SolidStatusBarConfig( + serverInfo: SolidServerInfo(serverUri: SolidConfig.defaultServerUrl), + loginStatus: SolidLoginStatus(), + securityKeyStatus: SolidSecurityKeyStatus(), + ), + aboutConfig: SolidAboutConfig( + applicationName: appTitle.split(' - ')[0], + applicationIcon: Image.asset( + 'assets/images/app_icon.png', + width: 64, + height: 64, + ), + applicationLegalese: ''' + + © {{appName}} + + ''', + text: ''' + + {{appName}} is a file browser application that allows you to manage + files on your personal online data store (Pod) hosted on a Solid + server. + + Key features: + + 📂 Browse and manage files on your Solid POD; + + 📤 Upload files to your POD; + + 📥 Download files from your POD; + + 🔐 Security key management for encrypted data; + + 🎨 Theme switching (light/dark/system); + + 🧭 Responsive navigation (rail ↔ drawer). + + Built with [solidpod](https://pub.dev/packages/solidpod) and + [solidui](https://pub.dev/packages/solidui) for the + [Australian Solid Community](https://solidcommunity.au). + + ''', + ), + themeToggle: const SolidThemeToggleConfig( + enabled: true, + showInAppBarActions: true, + ), + inviteConfig: inviteOthersConfig, + child: const Home(title: appTitle), + ); + } +} diff --git a/templates/solidui/lib/constants/app.dart.tmpl b/templates/solidui/lib/constants/app.dart.tmpl new file mode 100644 index 0000000..0493bf7 --- /dev/null +++ b/templates/solidui/lib/constants/app.dart.tmpl @@ -0,0 +1,59 @@ +/// {{appName}} - app-wide constants. +/// +/// This file was generated from the `solidui` app template +/// (`dart run solidui:create`). Edit it freely to suit your app. + +library; + +import 'package:solidui/solidui.dart' + show SolidFileUploadConfig, SolidInviteOthersConfig; + +/// Application title displayed as the window title. + +const String appTitle = '{{appTitle}}'; + +/// Shared upload configuration for every `SolidFile` view in {{appName}}. +/// +/// Restricts the file picker (both the toolbar Upload button and the side +/// upload panel) to Markdown and plain text files. Extensions are matched +/// case-insensitively by SolidUI, so users may still pick `.MD` / `.TXT`. +/// Adjust `allowedExtensions` to suit the file types your app manages. + +const SolidFileUploadConfig appUploadConfig = SolidFileUploadConfig( + allowedExtensions: ['md', 'txt'], +); + +/// Public URL where {{appName}} is hosted. Used by the Invite Others +/// feature to send a working link to the recipient. + +const String appUrl = 'https://{{projectName}}.solidcommunity.au/'; + +/// Application-wide Invite Others configuration shared by the +/// AppBar share button and the App Info dialog so that users can +/// invite others to set up their POD and try {{appName}}. + +const SolidInviteOthersConfig inviteOthersConfig = SolidInviteOthersConfig( + applicationName: '{{appName}}', + appUrl: appUrl, + appDescription: + 'manage/share resources hosted on your Solid server using {{appName}}', + messageTemplate: ''' +You might like to try the {appName} app, available online here: + +{appUrl} + +Signing into {appName} will set up your data vault so you can manage and +exchange files privately with other Solid users. + +''', + subject: 'Try the {{appName}} app on your Solid POD', + tooltip: ''' + + **Invite Others** + + Tap to invite someone else to try {{appName}}. You can copy the + invitation to the clipboard or share it through any messaging app + installed on your device. + + ''', +); diff --git a/templates/solidui/lib/home.dart.tmpl b/templates/solidui/lib/home.dart.tmpl new file mode 100644 index 0000000..3a12e2b --- /dev/null +++ b/templates/solidui/lib/home.dart.tmpl @@ -0,0 +1,71 @@ +/// {{appName}} - the application introductory home page. +/// +/// This file was generated from the `solidui` app template +/// (`dart run solidui:create`). Edit it freely to suit your app. + +library; + +import 'package:flutter/material.dart'; + +class Home extends StatefulWidget { + const Home({super.key, required this.title}); + + final String title; + + @override + State createState() => _HomeState(); +} + +class _HomeState extends State { + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Center( + child: Card( + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.folder_open, + size: 64, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: 16), + Text( + widget.title, + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 24), + Text( + ''' +Welcome to {{appName}}! + +{{appName}} is a Solid file browser that lets you manage +files on your personal online data store (POD). + +Key features: + +• Browse files and folders on your Solid POD +• Upload files to your POD +• Download files from your POD +• View all POD files from the root +• Security key management for encrypted data +• Theme switching (light/dark/system) +• Responsive navigation (rail ↔ drawer) + +Use the navigation menu to explore your POD files! + ''', + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/templates/solidui/lib/main.dart.tmpl b/templates/solidui/lib/main.dart.tmpl new file mode 100644 index 0000000..5622b9a --- /dev/null +++ b/templates/solidui/lib/main.dart.tmpl @@ -0,0 +1,64 @@ +/// {{appName}} - main entry point for the application. +/// +/// This file was generated from the `solidui` app template +/// (`dart run solidui:create`). Edit it freely to suit your app. + +library; + +import 'package:flutter/material.dart'; + +import 'package:solidui/solidui.dart'; +import 'package:window_manager/window_manager.dart'; + +import 'package:{{projectName}}/app.dart'; +import 'package:{{projectName}}/constants/app.dart'; + +// The main entry point for the application. We require [async] here because we +// asynchronously [await] the window manager below. Eventually `main()` hands +// over to the widget passed to [runApp]. + +void main() async { + // Optionally, during development, we can use [debugPrint] to trace + // execution. The output is not shown on a `flutter --release`. To quieten the + // `flutter --debug` output we can globally map [debugPrint] to a no-op: + // + // debugPrint = (String? message, {int? wrapWidth}) { + // null; + // }; + + // ── Desktop setup ────────────────────────────────────────────────────────── + + // We ensure the Flutter bindings are initialised for the async operations + // below, particularly to set the desktop window [title]. + + WidgetsFlutterBinding.ensureInitialized(); + + if (isDesktop) { + await windowManager.ensureInitialized(); + + // For the desktop app we tune various window oriented settings. These are + // not required for mobile apps. + + const windowOptions = WindowOptions( + title: appTitle, + minimumSize: Size(500, 800), + backgroundColor: Colors.transparent, + skipTaskbar: false, + titleBarStyle: TitleBarStyle.normal, + ); + + // Await the window being shown and receiving focus before we run the app. + + await windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.show(); + await windowManager.focus(); + }); + } + + // ── Run the app ──────────────────────────────────────────────────────────── + + // The runApp() function takes the given Widget and makes it the root of the + // tree of widgets that the app creates. + + runApp(const App()); +} diff --git a/templates/solidui/lib/screens/browse_files.dart.tmpl b/templates/solidui/lib/screens/browse_files.dart.tmpl new file mode 100644 index 0000000..b2f737f --- /dev/null +++ b/templates/solidui/lib/screens/browse_files.dart.tmpl @@ -0,0 +1,28 @@ +/// {{appName}} - display all folders from the root of a user's pod. +/// +/// This file was generated from the `solidui` app template +/// (`dart run solidui:create`). Edit it freely to suit your app. + +library; + +import 'package:flutter/material.dart'; + +import 'package:solidui/solidui.dart'; + +import 'package:{{projectName}}/constants/app.dart'; + +class BrowseFiles extends StatelessWidget { + const BrowseFiles({super.key}); + + @override + Widget build(BuildContext context) { + // SolidFile() from `solidui` is a comprehensive file browser for the + // resources contained in your data vault hosted on any Solid server. + + return const SolidFile( + currentPath: SolidFile.podRoot, + friendlyFolderName: 'All Files and Folders', + uploadConfig: appUploadConfig, + ); + } +} diff --git a/templates/solidui/macos/Runner/DebugProfile.entitlements b/templates/solidui/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..4d3b342 --- /dev/null +++ b/templates/solidui/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,20 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + keychain-access-groups + + com.apple.security.keychain + + com.apple.security.files.user-selected.read-write + + + diff --git a/templates/solidui/macos/Runner/Release.entitlements b/templates/solidui/macos/Runner/Release.entitlements new file mode 100644 index 0000000..4d3b342 --- /dev/null +++ b/templates/solidui/macos/Runner/Release.entitlements @@ -0,0 +1,20 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + keychain-access-groups + + com.apple.security.keychain + + com.apple.security.files.user-selected.read-write + + + diff --git a/templates/solidui/pubspec.yaml.tmpl b/templates/solidui/pubspec.yaml.tmpl new file mode 100644 index 0000000..f7c019c --- /dev/null +++ b/templates/solidui/pubspec.yaml.tmpl @@ -0,0 +1,54 @@ +name: {{projectName}} +description: {{appDescription}} +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: '>=3.2.3 <4.0.0' + flutter: '>=3.10.0' + +# To automatically upgrade package dependencies: +# +# flutter pub upgrade --major-versions +# +# To see which dependencies have newer versions available: +# +# flutter pub outdated + +dependencies: + flutter: + sdk: flutter + solidui: ^1.0.14 + window_manager: ^0.5.1 + +dev_dependencies: + flutter_launcher_icons: ^0.14.4 + flutter_lints: ^6.0.0 + +flutter: + assets: + - assets/images/app_icon.png + - assets/images/app_image.jpg + uses-material-design: true + +# Define launcher icons for all platforms (except Linux) so the app icons can +# be regenerated using: +# +# dart run flutter_launcher_icons + +flutter_launcher_icons: + image_path: "assets/images/app_icon.png" + android: true + min_sdk_android: 21 + ios: true + remove_alpha_ios: true + background_color_ios: "#ffffff" + macos: + generate: true + web: + generate: true + background_color: "#ffffff" + theme_color: "#ffffff" + windows: + generate: true + icon_size: 48 # min:48, max:256, default: 48 diff --git a/templates/solidui/solid/README.md.tmpl b/templates/solidui/solid/README.md.tmpl new file mode 100644 index 0000000..2f30817 --- /dev/null +++ b/templates/solidui/solid/README.md.tmpl @@ -0,0 +1,48 @@ +# Solid-OIDC client registration for {{appName}} + +**Login will not work until the two files in this folder are published on the +public web at the location your `clientId` points to.** This is the single most +common reason a freshly generated Solid app cannot complete login: the identity +provider fetches the `clientId` URL to learn which `redirect_uris` are allowed, +and if that document is missing (HTTP 404) it refuses to hand control back to +the app after the consent screen (you will see an +`ASWebAuthenticationSession Code=1` / cancelled error). + +This folder contains: + +- `client-profile.jsonld` — the Solid-OIDC Client Identifier Document. Its + `redirect_uris` exactly match those passed to `SolidLogin` in `lib/app.dart`. +- `redirect.html` — the web/post-logout redirect helper used by the `oidc` + package (only needed for the web build, but deploy it too). + +## Where to publish + +The app currently uses: + +``` +clientId: https://solidcommunity.au/apps/{{projectName}}/client-profile.jsonld +``` + +Publish both files so that they are reachable (HTTP 200, public, no auth) at: + +``` +https://solidcommunity.au/apps/{{projectName}}/client-profile.jsonld +https://solidcommunity.au/apps/{{projectName}}/redirect.html +``` + +- **If you maintain solidcommunity.au** (the ANU Solid Community), deploy these + to `apps/{{projectName}}/` alongside the other apps (this is how `filepod` is set + up). +- **Otherwise**, host them on any public URL you control (your own Pod's public + folder, a static site, GitHub Pages, …) and then update `clientId` **and** the + matching `https://…/redirect.html` entry in `lib/app.dart` to that URL. + +## Verify + +```bash +curl -I https://solidcommunity.au/apps/{{projectName}}/client-profile.jsonld # expect 200 +``` + +The `redirect_uris` in the published document must be byte-for-byte identical to +the `redirectUris` list in `lib/app.dart`, including the +`{{orgName}}.{{schemeName}}://redirect` custom scheme. diff --git a/templates/solidui/solid/client-profile.jsonld.tmpl b/templates/solidui/solid/client-profile.jsonld.tmpl new file mode 100644 index 0000000..fb40afd --- /dev/null +++ b/templates/solidui/solid/client-profile.jsonld.tmpl @@ -0,0 +1,25 @@ +{ + "@context": "https://www.w3.org/ns/solid/oidc-context.jsonld", + "client_id": "https://solidcommunity.au/apps/{{projectName}}/client-profile.jsonld", + "client_name": "{{appName}}", + "application_type": "native", + "redirect_uris": [ + "https://solidcommunity.au/apps/{{projectName}}/redirect.html", + "{{orgName}}.{{schemeName}}://redirect", + "http://localhost:4400/redirect" + ], + "post_logout_redirect_uris": [ + "https://solidcommunity.au/apps/{{projectName}}/redirect.html", + "{{orgName}}.{{schemeName}}://redirect", + "http://localhost:4400/redirect" + ], + "scope": "openid profile offline_access webid", + "grant_types": [ + "authorization_code", + "refresh_token" + ], + "response_types": [ + "code" + ], + "token_endpoint_auth_method": "none" +} diff --git a/templates/solidui/solid/redirect.html b/templates/solidui/solid/redirect.html new file mode 100644 index 0000000..3d64b43 --- /dev/null +++ b/templates/solidui/solid/redirect.html @@ -0,0 +1,122 @@ + + + + + + + Flutter Oidc Redirect + + + + + +

Authentication completed! Please close this page.

+ + +