Files
Client/lib/push/push_registration_store.dart
T

151 lines
6.0 KiB
Dart

import 'push_registration_type.dart';
import 'push_secure_storage.dart';
/// Persists the bookkeeping produced by successful push registrations —
/// per [PushRegistrationType]: the Nextcloud device identifier, the FCM token
/// the registration was made with (so a token refresh can be detected), the
/// endpoints it was bound to (so an endpoint switch in the dev tools can be
/// detected) and the last attempt outcome. The server public key and the
/// device keypair are shared between both registrations.
///
/// Key layout: the `general` type uses the pre-dual key names unchanged, so
/// existing installs are implicitly migrated — their stored registration IS
/// the general one; the missing talk registration is added by the next
/// register-on-start self-heal.
class PushRegistrationStore {
static const _deviceIdentifierKey = 'push_device_identifier';
static const _serverPublicKeyKey = 'push_server_public_key_pem';
static const _registeredTokenKey = 'push_registered_fcm_token';
static const _proxyServerKey = 'push_registered_proxy_server';
static const _ncBaseUrlKey = 'push_registered_nc_base_url';
static const _lastAttemptAtKey = 'push_last_registration_at';
static const _lastAttemptErrorKey = 'push_last_registration_error';
// Native-only context: the iOS AppDelegate answers Talk notification actions
// (reply / mark-as-read) directly via URLSession while the Flutter engine is
// not guaranteed to run. It needs the Nextcloud username and base URL from the
// shared (group-scoped) keychain; the app password already lives there
// (AccountData writes `nextcloud_app_password` group-scoped).
static const _usernameKey = 'nextcloud_username';
static const _baseUrlKey = 'nextcloud_base_url';
static const _perTypeKeys = [
_deviceIdentifierKey,
_registeredTokenKey,
_proxyServerKey,
_ncBaseUrlKey,
_lastAttemptAtKey,
_lastAttemptErrorKey,
];
final FlutterSecureStorageLike _storage;
const PushRegistrationStore([this._storage = const PushSecureStorage()]);
/// Type-specific key: general keeps the legacy names (implicit migration of
/// pre-dual installs), talk appends a suffix.
static String keyFor(String baseKey, PushRegistrationType type) =>
type == PushRegistrationType.general ? baseKey : '${baseKey}_talk';
Future<void> save({
required PushRegistrationType type,
required String deviceIdentifier,
required String serverPublicKeyPem,
required String fcmToken,
required String proxyServer,
required String ncBaseUrl,
}) async {
await _storage.write(
key: keyFor(_deviceIdentifierKey, type),
value: deviceIdentifier,
);
await _storage.write(key: _serverPublicKeyKey, value: serverPublicKeyPem);
await _storage.write(
key: keyFor(_registeredTokenKey, type),
value: fcmToken,
);
await _storage.write(
key: keyFor(_proxyServerKey, type),
value: proxyServer,
);
await _storage.write(key: keyFor(_ncBaseUrlKey, type), value: ncBaseUrl);
}
/// Persists the username and Nextcloud base URL group-scoped so the native
/// iOS Talk action handler (AppDelegate) can authenticate OCS calls. The base
/// URL is a full origin like `https://cloud.marianum-fulda.de` (domain +
/// optional path, no trailing slash).
Future<void> saveNativeAuthContext({
required String username,
required String baseUrl,
}) async {
await _storage.write(key: _usernameKey, value: username);
await _storage.write(key: _baseUrlKey, value: baseUrl);
}
Future<String?> deviceIdentifier(PushRegistrationType type) =>
_storage.read(key: keyFor(_deviceIdentifierKey, type));
/// Per-user server public key — identical for both registrations.
Future<String?> serverPublicKeyPem() =>
_storage.read(key: _serverPublicKeyKey);
Future<String?> registeredFcmToken(PushRegistrationType type) =>
_storage.read(key: keyFor(_registeredTokenKey, type));
/// Proxy-server URL the registration of [type] was made with.
Future<String?> registeredProxyServer(PushRegistrationType type) =>
_storage.read(key: keyFor(_proxyServerKey, type));
/// Nextcloud base URL the registration of [type] was made against.
Future<String?> registeredNcBaseUrl(PushRegistrationType type) =>
_storage.read(key: keyFor(_ncBaseUrlKey, type));
/// True when a registration of [type] has been persisted.
Future<bool> isRegistered(PushRegistrationType type) async =>
(await registeredFcmToken(type))?.isNotEmpty ?? false;
/// Records the outcome of the most recent registration attempt of [type] so
/// the push status view can show when it ran and why it failed. [error]
/// null = success (stored as empty string).
Future<void> saveLastRegistrationAttempt({
required PushRegistrationType type,
required DateTime at,
String? error,
}) async {
await _storage.write(
key: keyFor(_lastAttemptAtKey, type),
value: at.toIso8601String(),
);
await _storage.write(
key: keyFor(_lastAttemptErrorKey, type),
value: error ?? '',
);
}
/// Timestamp of the last registration attempt of [type], or null when none
/// ran yet.
Future<DateTime?> lastRegistrationAt(PushRegistrationType type) async {
final raw = await _storage.read(key: keyFor(_lastAttemptAtKey, type));
if (raw == null || raw.isEmpty) return null;
return DateTime.tryParse(raw);
}
/// Error text of the last registration attempt of [type], or null when it
/// succeeded (or never ran).
Future<String?> lastRegistrationError(PushRegistrationType type) async {
final raw = await _storage.read(key: keyFor(_lastAttemptErrorKey, type));
return (raw == null || raw.isEmpty) ? null : raw;
}
Future<void> clear() async {
for (final type in PushRegistrationType.values) {
for (final key in _perTypeKeys) {
await _storage.delete(key: keyFor(key, type));
}
}
await _storage.delete(key: _serverPublicKeyKey);
await _storage.delete(key: _usernameKey);
await _storage.delete(key: _baseUrlKey);
}
}