300 lines
9.8 KiB
Dart
300 lines
9.8 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:app_settings/app_settings.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
import '../../../../api/errors/error_mapper.dart';
|
|
import '../../../../api/marianumconnect/queries/push_device_test/push_device_test.dart';
|
|
import '../../../../extensions/date_time.dart';
|
|
import '../../../../push/push_registration.dart';
|
|
import '../../../../push/push_status.dart';
|
|
import '../../../../state/app/modules/capabilities/bloc/capabilities_cubit.dart';
|
|
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
|
import '../../../../widget/app_progress_indicator.dart';
|
|
import '../../../../widget/demo_restricted.dart';
|
|
import '../../../../widget/details_bottom_sheet.dart';
|
|
|
|
/// Opens the push status checklist: one row per link in the push chain with
|
|
/// an explanation where it is broken, the last registration attempt (incl.
|
|
/// verbatim error), a manual re-register action and — once the chain is
|
|
/// operational — a test notification. Loads once on open; the refresh action
|
|
/// re-collects on demand (no polling).
|
|
Future<void> showPushStatusSheet(BuildContext context) {
|
|
// Captured here: the sheet outlives this build context's element tree.
|
|
final settings = context.read<SettingsCubit>();
|
|
final capabilities = context.read<CapabilitiesCubit>();
|
|
return showDetailsBottomSheet(
|
|
context,
|
|
header: const ListTile(
|
|
leading: Icon(Icons.monitor_heart_outlined),
|
|
title: Text('Status der Push-Benachrichtigungen'),
|
|
),
|
|
children: (sheetCtx) => [
|
|
_PushStatusBody(settings: settings, capabilities: capabilities),
|
|
],
|
|
);
|
|
}
|
|
|
|
class _PushStatusBody extends StatefulWidget {
|
|
final SettingsCubit settings;
|
|
final CapabilitiesCubit capabilities;
|
|
|
|
const _PushStatusBody({required this.settings, required this.capabilities});
|
|
|
|
@override
|
|
State<_PushStatusBody> createState() => _PushStatusBodyState();
|
|
}
|
|
|
|
class _PushStatusBodyState extends State<_PushStatusBody>
|
|
with WidgetsBindingObserver {
|
|
PushStatusReport? _report;
|
|
bool _busy = false;
|
|
|
|
/// Set while the user is in the OS settings — the next `resumed` lifecycle
|
|
/// event then re-collects the status so a just-changed permission shows up.
|
|
bool _reloadOnResume = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addObserver(this);
|
|
_load();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
if (state == AppLifecycleState.resumed && _reloadOnResume) {
|
|
_reloadOnResume = false;
|
|
unawaited(_load());
|
|
}
|
|
}
|
|
|
|
Future<void> _openNotificationSettings() async {
|
|
_reloadOnResume = true;
|
|
await AppSettings.openAppSettings(type: AppSettingsType.notification);
|
|
}
|
|
|
|
Future<void> _load() async {
|
|
final capabilitiesState = widget.capabilities.state;
|
|
final report = await collectPushStatus(
|
|
settingEnabled: widget.settings.val().notificationSettings.enabled,
|
|
capabilityPush: capabilitiesState.pushNotifications,
|
|
capabilitiesLoaded: capabilitiesState.loaded,
|
|
);
|
|
if (!mounted) return;
|
|
setState(() => _report = report);
|
|
}
|
|
|
|
Future<void> _reRegister() async {
|
|
if (guardDemoAction(context)) return;
|
|
if (_busy) return;
|
|
setState(() => _busy = true);
|
|
final messenger = ScaffoldMessenger.of(context);
|
|
final ok = await PushRegistration().register();
|
|
if (!mounted) return;
|
|
setState(() => _busy = false);
|
|
await _load();
|
|
messenger.showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
ok
|
|
? 'Registrierung erfolgreich abgeschlossen'
|
|
: 'Registrierung fehlgeschlagen — Details in der Statusübersicht',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _sendTest() async {
|
|
if (guardDemoAction(context)) return;
|
|
if (_busy) return;
|
|
setState(() => _busy = true);
|
|
final messenger = ScaffoldMessenger.of(context);
|
|
String message;
|
|
try {
|
|
final devices = await PushDeviceTest().run();
|
|
message = switch (devices) {
|
|
0 => 'Es ist kein Gerät registriert — bitte erneut registrieren',
|
|
1 => 'Testbenachrichtigung an 1 Gerät gesendet',
|
|
_ => 'Testbenachrichtigung an $devices Geräte gesendet',
|
|
};
|
|
} on Object catch (e) {
|
|
message = errorToUserMessage(e);
|
|
}
|
|
if (!mounted) return;
|
|
setState(() => _busy = false);
|
|
messenger.showSnackBar(SnackBar(content: Text(message)));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final report = _report;
|
|
if (report == null) {
|
|
return const Padding(
|
|
padding: EdgeInsets.all(32),
|
|
child: Center(child: AppProgressIndicator.medium()),
|
|
);
|
|
}
|
|
final theme = Theme.of(context);
|
|
final rows = buildPushStatusRows(report);
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
...rows.map((row) {
|
|
// Offer the OS-settings shortcut on the permission row whenever it
|
|
// isn't granted — after a denial that's the only place to fix it.
|
|
final canOpenSettings =
|
|
row.opensNotificationSettings && row.state != PushCheck.ok;
|
|
return ListTile(
|
|
dense: true,
|
|
leading: _stateIcon(row.state, theme),
|
|
title: Text(row.label),
|
|
subtitle: row.detail == null ? null : Text(row.detail!),
|
|
trailing: canOpenSettings
|
|
? TextButton(
|
|
onPressed: _openNotificationSettings,
|
|
child: const Text('Einstellungen'),
|
|
)
|
|
: null,
|
|
);
|
|
}),
|
|
if (report.general.lastRegistrationAt != null ||
|
|
report.talk.lastRegistrationAt != null)
|
|
const Divider(height: 1),
|
|
..._lastAttempt(theme, 'Allgemein', report.general),
|
|
..._lastAttempt(theme, 'Talk', report.talk),
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
|
child: _actions(report),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Last-attempt line (+ verbatim error) for one registration type.
|
|
List<Widget> _lastAttempt(
|
|
ThemeData theme,
|
|
String label,
|
|
PushTypeStatus status,
|
|
) {
|
|
final at = status.lastRegistrationAt;
|
|
if (at == null) return const [];
|
|
final error = status.lastRegistrationError;
|
|
return [
|
|
ListTile(
|
|
dense: true,
|
|
leading: Icon(
|
|
error == null ? Icons.history : Icons.error_outline,
|
|
color: error == null ? null : theme.colorScheme.error,
|
|
),
|
|
title: Text(
|
|
'Letzte Registrierung ($label): ${at.formatDateTime()} — '
|
|
'${error == null ? 'erfolgreich' : 'fehlgeschlagen'}',
|
|
),
|
|
),
|
|
if (error != null)
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.surfaceContainerHighest,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
error,
|
|
style: TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 12,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
];
|
|
}
|
|
|
|
/// Action hierarchy: refresh stays a secondary icon action; the primary
|
|
/// (filled) button is the test notification once the chain is operational,
|
|
/// otherwise re-registering IS the primary next step and the test action is
|
|
/// omitted (it could not succeed and the checklist explains why).
|
|
Widget _actions(PushStatusReport report) {
|
|
const spinner = SizedBox(
|
|
width: 16,
|
|
height: 16,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
);
|
|
final ready = report.readyForTestNotification;
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
IconButton(
|
|
onPressed: _busy ? null : _load,
|
|
tooltip: 'Aktualisieren',
|
|
icon: const Icon(Icons.refresh),
|
|
),
|
|
// OverflowBar stacks the buttons vertically on narrow screens instead
|
|
// of overflowing the row.
|
|
Expanded(
|
|
child: OverflowBar(
|
|
alignment: MainAxisAlignment.end,
|
|
overflowAlignment: OverflowBarAlignment.end,
|
|
spacing: 8,
|
|
overflowSpacing: 4,
|
|
children: [
|
|
if (ready) ...[
|
|
TextButton(
|
|
onPressed: _busy ? null : _reRegister,
|
|
child: const Text('Erneut registrieren'),
|
|
),
|
|
FilledButton.icon(
|
|
onPressed: _busy ? null : _sendTest,
|
|
icon: _busy ? spinner : const Icon(Icons.send_outlined),
|
|
label: const Text('Testbenachrichtigung'),
|
|
),
|
|
] else
|
|
FilledButton.icon(
|
|
onPressed: _busy ? null : _reRegister,
|
|
icon: _busy ? spinner : const Icon(Icons.sync),
|
|
label: const Text('Erneut registrieren'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _stateIcon(PushCheck state, ThemeData theme) {
|
|
switch (state) {
|
|
case PushCheck.ok:
|
|
return const Icon(
|
|
Icons.check_circle_outline,
|
|
color: Colors.green,
|
|
semanticLabel: 'In Ordnung',
|
|
);
|
|
case PushCheck.fail:
|
|
return Icon(
|
|
Icons.cancel_outlined,
|
|
color: theme.colorScheme.error,
|
|
semanticLabel: 'Fehler',
|
|
);
|
|
case PushCheck.unknown:
|
|
return Icon(
|
|
Icons.remove_circle_outline,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
semanticLabel: 'Unbekannt',
|
|
);
|
|
}
|
|
}
|
|
}
|