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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions lib/src/utils/solid_alert.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,36 @@ library;

import 'package:flutter/material.dart';

/// Alert widget to popup a dialog with a given message and an optional title.
/// Approximate average width, in logical pixels, of one character at the
/// default Material body text size (~14 sp). Used to translate a
/// "characters per line" budget into an actual `maxWidth` constraint for
/// alert dialogs. Slightly conservative so wider glyphs (e.g. `m`, `W`)
/// still tend to fit within the requested character budget.

const double _approxCharWidthLogicalPixels = 7.0;

/// Default character-per-line cap applied to alert dialogs raised by
/// [alert]. ~90 characters keeps long error messages readable on wide
/// desktop windows without forcing short messages to look awkwardly narrow
/// on phones; it also matches the 80–100 character convention familiar
/// from prose and source-code line lengths.

const int defaultAlertMaxCharsPerLine = 90;

/// Convert a "characters per line" budget into a logical-pixel `maxWidth`
/// suitable for use with [BoxConstraints]. Shared by [alert] and any
/// custom alert dialogs that want a consistent reading width.

double alertMaxWidthForCharsPerLine(int maxCharsPerLine) =>
maxCharsPerLine * _approxCharWidthLogicalPixels;

/// Pop up a dismissable alert dialog with the given [msg].
///
/// [title] defaults to `'Alert'`. The message column is capped at
/// approximately [defaultAlertMaxCharsPerLine] characters wide so that long
/// messages do not stretch across the full width of a desktop window.
/// Short messages are unaffected — the cap only takes effect when the
/// natural width of the text exceeds it.

Future<void> alert(
BuildContext context,
Expand All @@ -41,7 +70,12 @@ Future<void> alert(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(msg),
content: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: alertMaxWidthForCharsPerLine(defaultAlertMaxCharsPerLine),
),
child: Text(msg),
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
Expand Down
183 changes: 183 additions & 0 deletions lib/src/widgets/grant_permission_dialogs.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/// Dialog helpers used by the grant permission form.
///
/// Copyright (C) 2024-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: Jess Moore, Anushka Vidanage, Tony Chen

library;

import 'package:flutter/material.dart';

import 'package:markdown_tooltip/markdown_tooltip.dart';
import 'package:solidpod/solidpod.dart' show WebIdCheckResult, WebIdCheckStatus;

import 'package:solidui/src/utils/solid_alert.dart'
show alertMaxWidthForCharsPerLine, defaultAlertMaxCharsPerLine;
import 'package:solidui/src/widgets/grant_permission_helpers_ui.dart'
show podNotInitMsg;
import 'package:solidui/src/widgets/solid_invite_others.dart';
import 'package:solidui/src/widgets/solid_invite_others_models.dart';

/// Shows a dismissable error dialog whose message column is constrained to
/// approximately [maxCharsPerLine] characters wide.
///
/// The default ([defaultAlertMaxCharsPerLine], ~90 characters) matches the
/// width used by the shared [alert] helper so error dialogs raised from
/// the grant permission flow have the same reading width as alerts raised
/// elsewhere in the app. Callers can pass a smaller value for short,
/// narrow notices.

Future<void> showGrantPermissionErrorDialog(
BuildContext context,
String title,
String message, {
int maxCharsPerLine = defaultAlertMaxCharsPerLine,
}) async {
await showDialog<void>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(title),
content: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: alertMaxWidthForCharsPerLine(maxCharsPerLine),
),
child: Text(message),
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text('OK'),
),
],
),
);
}

/// Handles the case where granting failed because one or more
/// recipients have not yet set up their POD. When an
/// [SolidInviteOthersConfig] is provided, the user is offered a
/// follow-up option to send the application's invitation directly.
/// Otherwise the original snackbar behaviour is kept so existing call
/// sites continue to work.

Future<void> handleNotInitialisedRecipients(
BuildContext context,
SolidInviteOthersConfig? inviteConfig,
) async {
if (inviteConfig == null) {
await showGrantPermissionErrorDialog(
context,
'Recipient POD not initialised',
podNotInitMsg,
);
return;
}

if (!context.mounted) return;
final shouldInvite = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Recipient has not set up a POD'),
// Cap the message column at ~90 characters (within the 80–100
// character convention used by the shared [alert] helper) so the
// dialog reads comfortably on wide desktop windows rather than
// stretching across the full window width.
content: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: alertMaxWidthForCharsPerLine(defaultAlertMaxCharsPerLine),
),
child: const Text(
'One or more of the WebIDs you entered have not yet '
'initialised their POD. Ask them to log in once to set up '
'their data vault — then you can grant access. Would you '
'like to send them an invitation now?',
),
),
actions: [
MarkdownTooltip(
message: '''

**Not now**

Dismiss this dialog without sending an invitation. You
can grant access again once the recipient has logged
into the app and set up their POD.

''',
child: TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text('Not now'),
),
),
MarkdownTooltip(
message: '''

**Invite this user**

Open the Invite Others dialog so you can send the
recipient a link to the app, prompting them to set up
their data vault.

''',
child: TextButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('Invite'),
),
),
],
),
);

if (!context.mounted) return;
if (shouldInvite == true) {
await InviteOthersDialog.show(context, config: inviteConfig);
}
}

/// Priority order used when several WebIDs in the group list fail. We
/// surface a single dialog and prefer the most actionable failure mode.

const List<WebIdCheckStatus> _groupReportPriority = [
WebIdCheckStatus.invalidIpv4,
WebIdCheckStatus.unreachable,
WebIdCheckStatus.notProfile,
];

/// Pick the [WebIdCheckResult] to surface in a single dialog when more
/// than one WebID in the group has failed. Higher-priority statuses are
/// preferred (see [_groupReportPriority]); otherwise the first failure
/// in input order is returned.

(String, WebIdCheckResult) pickGroupFailureToReport(
List<(String, WebIdCheckResult)> failures,
) {
assert(failures.isNotEmpty);
for (final status in _groupReportPriority) {
for (final failure in failures) {
if (failure.$2.status == status) return failure;
}
}
return failures.first;
}
Loading
Loading