Files
Client/lib/push/push_status.dart
T

296 lines
9.9 KiB
Dart

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import '../model/account_data.dart';
import 'push_keypair.dart';
import 'push_registration.dart';
import 'push_registration_store.dart';
import 'push_registration_type.dart';
/// Tri-state result of a single push-chain check.
enum PushCheck { ok, fail, unknown }
/// State of one of the two registrations (general/talk) — Nextcloud binding,
/// proxy binding and last attempt outcome.
@immutable
class PushTypeStatus {
/// Nextcloud subscription present (device identifier stored).
final bool nextcloudRegistered;
final String? registeredNcBaseUrl;
final String? registeredProxyServer;
final DateTime? lastRegistrationAt;
/// Error text of the last registration attempt; null = success or never ran.
final String? lastRegistrationError;
const PushTypeStatus({
required this.nextcloudRegistered,
required this.registeredNcBaseUrl,
required this.registeredProxyServer,
required this.lastRegistrationAt,
required this.lastRegistrationError,
});
}
/// Snapshot of every link in the push chain, in delivery order. Pure data —
/// the display rows are derived by [buildPushStatusRows] so the mapping is
/// unit-testable without any plugin.
@immutable
class PushStatusReport {
final bool settingEnabled;
final PushCheck osPermission;
/// Backend capability `pushNotifications`; [PushCheck.unknown] while the
/// capabilities have not been loaded yet this session.
final PushCheck serverCapability;
final bool appPasswordPresent;
final bool talkAppPasswordPresent;
final bool keypairPresent;
/// State of the general (apptype unknown) registration.
final PushTypeStatus general;
/// State of the Talk-classified registration.
final PushTypeStatus talk;
final String? currentProxyServer;
const PushStatusReport({
required this.settingEnabled,
required this.osPermission,
required this.serverCapability,
required this.appPasswordPresent,
required this.talkAppPasswordPresent,
required this.keypairPresent,
required this.general,
required this.talk,
required this.currentProxyServer,
});
/// True when the stored proxy binding of [status] diverges from the active
/// endpoint.
bool proxyEndpointMismatch(PushTypeStatus status) =>
currentProxyServer != null &&
PushRegistration.endpointChanged(
registered: status.registeredProxyServer,
current: currentProxyServer!,
);
/// True when a test notification can actually be delivered. The test push
/// is a Connect direct push routed via the general registration, so a
/// healthy general chain suffices — a broken talk registration only affects
/// Talk message delivery. The OS permission must not be denied (`unknown`
/// stays permissive, mirroring [PushRegistration.isPermissionUsable]).
bool get readyForTestNotification =>
osPermission != PushCheck.fail &&
general.nextcloudRegistered &&
(general.registeredProxyServer?.isNotEmpty ?? false) &&
!proxyEndpointMismatch(general) &&
general.lastRegistrationError == null;
/// True when no link in the chain is currently broken. Unknown links stay
/// permissive (mirroring [readyForTestNotification]) so a not-yet-loaded
/// capability or an undetermined OS permission does not flip the at-a-glance
/// health icon to red. Drives the compact status indicator in the settings.
bool get chainHealthy =>
buildPushStatusRows(this).every((row) => row.state != PushCheck.fail);
}
/// Collects the current push chain state. Settings/capability flags come from
/// the caller (they live in cubits); everything else is read from the secure
/// stores and the messaging plugin.
Future<PushStatusReport> collectPushStatus({
required bool settingEnabled,
required bool capabilityPush,
required bool capabilitiesLoaded,
PushRegistrationStore store = const PushRegistrationStore(),
PushKeypair keypair = const PushKeypair(),
}) async {
final registration = PushRegistration();
String? currentProxy;
try {
currentProxy = registration.currentProxyServer;
} on Object {
currentProxy = null;
}
Future<PushTypeStatus> typeStatus(PushRegistrationType type) async =>
PushTypeStatus(
nextcloudRegistered:
(await store.deviceIdentifier(type))?.isNotEmpty ?? false,
registeredNcBaseUrl: await store.registeredNcBaseUrl(type),
registeredProxyServer: await store.registeredProxyServer(type),
lastRegistrationAt: await store.lastRegistrationAt(type),
lastRegistrationError: await store.lastRegistrationError(type),
);
return PushStatusReport(
settingEnabled: settingEnabled,
osPermission: await _osPermission(),
serverCapability: !capabilitiesLoaded
? PushCheck.unknown
: (capabilityPush ? PushCheck.ok : PushCheck.fail),
appPasswordPresent: AccountData().hasAppPassword(),
talkAppPasswordPresent: AccountData().hasAppPasswordTalk(),
keypairPresent: (await keypair.loadPublicKeyPem())?.isNotEmpty ?? false,
general: await typeStatus(PushRegistrationType.general),
talk: await typeStatus(PushRegistrationType.talk),
currentProxyServer: currentProxy,
);
}
Future<PushCheck> _osPermission() async {
try {
final settings = await FirebaseMessaging.instance.getNotificationSettings();
switch (settings.authorizationStatus) {
case AuthorizationStatus.authorized:
case AuthorizationStatus.provisional:
return PushCheck.ok;
case AuthorizationStatus.denied:
return PushCheck.fail;
case AuthorizationStatus.notDetermined:
return PushCheck.unknown;
}
} on Object {
return PushCheck.unknown;
}
}
/// One line in the status checklist.
@immutable
class PushStatusRow {
final String label;
final PushCheck state;
/// Short explanation shown as subtitle — set for failures (what to do) and
/// for informational details (e.g. the registered URL).
final String? detail;
/// When true, the row offers a shortcut into the OS notification settings —
/// the only place the user can (re)grant the permission after a denial.
final bool opensNotificationSettings;
const PushStatusRow({
required this.label,
required this.state,
this.detail,
this.opensNotificationSettings = false,
});
}
const _pendingDetail =
'Registrierung ausstehend — sie wird beim nächsten App-Start '
'automatisch wiederholt';
/// Derives the display checklist from a [PushStatusReport]. Pure — the order
/// mirrors the actual chain: setting → OS → server → credentials → keys →
/// Nextcloud (general/talk) → Connect (general/talk).
List<PushStatusRow> buildPushStatusRows(PushStatusReport r) => [
PushStatusRow(
label: 'Push-Benachrichtigungen aktiviert',
state: r.settingEnabled ? PushCheck.ok : PushCheck.fail,
detail: r.settingEnabled
? null
: 'In den Einstellungen deaktiviert — über den Schalter oben aktivieren',
),
PushStatusRow(
label: 'Benachrichtigungsberechtigung',
state: r.osPermission,
opensNotificationSettings: true,
detail: switch (r.osPermission) {
PushCheck.ok => null,
PushCheck.fail =>
'Die Benachrichtigungsberechtigung wurde in den Systemeinstellungen '
'deaktiviert',
PushCheck.unknown => 'Noch nicht erteilt',
},
),
PushStatusRow(
label: 'Server-Unterstützung',
state: r.serverCapability,
detail: switch (r.serverCapability) {
PushCheck.ok => null,
PushCheck.fail =>
'Der Server unterstützt Push-Benachrichtigungen derzeit nicht',
PushCheck.unknown => 'Serverinformationen noch nicht geladen',
},
),
PushStatusRow(
label: 'App-Passwörter',
state: r.appPasswordPresent && r.talkAppPasswordPresent
? PushCheck.ok
: PushCheck.fail,
detail: r.appPasswordPresent && r.talkAppPasswordPresent
? null
: '${_missingPasswords(r)} — wird beim nächsten App-Start '
'automatisch angefordert',
),
PushStatusRow(
label: 'Geräteschlüssel',
state: r.keypairPresent ? PushCheck.ok : PushCheck.fail,
detail: r.keypairPresent
? null
: 'Nicht vorhanden — wird bei der nächsten Registrierung erzeugt',
),
_nextcloudRow(r.general, 'Nextcloud-Registrierung (Allgemein)'),
_nextcloudRow(r.talk, 'Nextcloud-Registrierung (Talk)'),
_connectRow(r, r.general, 'Connect-Registrierung (Allgemein)'),
_connectRow(r, r.talk, 'Connect-Registrierung (Talk)'),
];
String _missingPasswords(PushStatusReport r) {
if (!r.appPasswordPresent && !r.talkAppPasswordPresent) {
return 'Beide fehlen';
}
return r.appPasswordPresent
? 'Talk-App-Passwort fehlt'
: 'Allgemeines App-Passwort fehlt';
}
PushStatusRow _nextcloudRow(PushTypeStatus status, String label) =>
PushStatusRow(
label: label,
state: status.nextcloudRegistered ? PushCheck.ok : PushCheck.fail,
detail: status.nextcloudRegistered
? status.registeredNcBaseUrl
: _pendingDetail,
);
PushStatusRow _connectRow(
PushStatusReport r,
PushTypeStatus status,
String label,
) {
final registeredProxy = status.registeredProxyServer;
if (registeredProxy == null || registeredProxy.isEmpty) {
return PushStatusRow(
label: label,
state: PushCheck.fail,
detail: _pendingDetail,
);
}
if (r.proxyEndpointMismatch(status)) {
return PushStatusRow(
label: label,
state: PushCheck.fail,
detail:
'Für $registeredProxy registriert — der aktive Server ist '
'${r.currentProxyServer}. Eine erneute Registrierung ist '
'erforderlich.',
);
}
if (status.lastRegistrationError != null) {
return PushStatusRow(
label: label,
state: PushCheck.fail,
detail: 'Die letzte Registrierung ist fehlgeschlagen — Details unten',
);
}
return PushStatusRow(
label: label,
state: PushCheck.ok,
detail: registeredProxy,
);
}