push registration no longer depends on notification permission, reporting effective opt-in and os permission

This commit is contained in:
2026-09-26 23:38:24 +02:00
parent e6f74ca6fe
commit 2aed3d104c
6 changed files with 129 additions and 81 deletions
+24 -32
View File
@@ -355,9 +355,8 @@ class PushRegistration {
status != AuthorizationStatus.deniedPermanently;
/// Requests the OS notification permission (covers iOS + Android 13) and
/// returns whether registration should proceed. Errors from the plugin are
/// treated as usable — better a possibly-idle registration than silently
/// losing push over a transient failure.
/// returns whether it is usable (not explicitly denied). Errors from the
/// plugin are treated as usable so a transient failure never nags the user.
static Future<bool> requestOsPermission() async {
try {
final settings = await FirebaseMessaging.instance.requestPermission();
@@ -368,54 +367,47 @@ class PushRegistration {
}
}
/// True when the user has explicitly denied the OS notification permission.
/// Read-only (no prompt) — used by the settings UI to surface the state.
static Future<bool> isOsPermissionDenied() async {
/// Current OS notification permission, or null when the plugin can't tell.
/// Read-only — never triggers the OS prompt.
static Future<AuthorizationStatus?> osPermissionStatus() async {
try {
final settings = await FirebaseMessaging.instance
.getNotificationSettings();
return !isPermissionUsable(settings.authorizationStatus);
return settings.authorizationStatus;
} on Object {
return false;
return null;
}
}
/// True when the OS notification permission is already granted
/// (`authorized`/`provisional`). Read-only — never triggers the OS prompt.
/// Used by the cold-start/self-heal path so it registers only for devices
/// that already opted in, leaving the actual prompt to the first Talk visit.
static Future<bool> isOsPermissionGranted() async {
try {
final settings = await FirebaseMessaging.instance
.getNotificationSettings();
return settings.authorizationStatus == AuthorizationStatus.authorized ||
settings.authorizationStatus == AuthorizationStatus.provisional;
} on Object {
return false;
}
final status = await osPermissionStatus();
return status != null && canDisplay(status);
}
/// Whether the OS will actually show a visible notification in [status].
/// Stricter than [isPermissionUsable]: `notDetermined` shows nothing yet.
static bool canDisplay(AuthorizationStatus status) =>
status == AuthorizationStatus.authorized ||
status == AuthorizationStatus.provisional;
/// Registers this device whenever the backend advertises the push capability.
/// Deliberately independent of the in-app notification toggle: a user who
/// turned notifications off stays registered so silent sync pushes keep
/// flowing — the display is suppressed downstream via the mirrored flag (see
/// [PushRegistrationStore.notificationsEnabled]). Only registers when the OS
/// notification permission is *already* granted — it never triggers the OS
/// prompt itself. Requesting the permission is the job of the first Talk visit
/// (see `maybePromptTalkNotifications`), which keeps the prompt out of the
/// cold-start path. Safe to call on every start — Nextcloud dedups an
/// unchanged registration — which also self-heals a device whose registration
/// was lost.
/// Deliberately independent of both the in-app notification toggle and the
/// OS notification permission: silent sync pushes (deletes, chat/badge
/// refresh, widget refresh) need no permission — data-only FCM on Android,
/// `content-available` on iOS — so every device stays in sync. Whether a
/// visible notification is wanted is reported separately via the telemetry
/// heartbeat (toggle + OS permission), from which the server picks silent
/// pushes on iOS. Never triggers the OS prompt. Safe to call on every
/// start — Nextcloud dedups an unchanged registration — which also
/// self-heals a device whose registration was lost.
/// Returns whether registration was actually *attempted* (all gates passed).
/// Even a partial success persists the `general` device identifier, so the
/// caller re-emits telemetry on `true` to reflect the fresh registration in
/// the same session instead of lagging until the next launch.
static Future<bool> syncSubscription({required bool capable}) async {
if (!capable) return false;
if (!await isOsPermissionGranted()) {
log('Push: OS notification permission not granted, skipping registration');
return false;
}
final registration = PushRegistration();
// register() below refreshes an unchanged subscription anyway; the check
// only surfaces the endpoint switch in the log for diagnosability.