296 lines
9.9 KiB
Dart
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,
|
|
);
|
|
}
|