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 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 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 _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 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, ); }