No notification permission enforcement on web

This commit is contained in:
Marc Rejohn Castillano 2026-02-25 18:39:39 +08:00
parent 0173e91d94
commit aef88de54b
3 changed files with 99 additions and 45 deletions

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
@ -56,17 +57,37 @@ Future<void> main() async {
// ensure token saved right away if already signed in // ensure token saved right away if already signed in
final supaClient = Supabase.instance.client; final supaClient = Supabase.instance.client;
final initialToken = await FirebaseMessaging.instance.getToken(); String? initialToken;
if (initialToken != null && supaClient.auth.currentUser != null) { if (!kIsWeb) {
debugPrint('initial FCM token for signed-in user: $initialToken'); try {
final ctrl = NotificationsController(supaClient); initialToken = await FirebaseMessaging.instance.getToken();
await ctrl.registerFcmToken(initialToken); } catch (e) {
debugPrint('FCM getToken failed: $e');
initialToken = null;
}
if (initialToken != null && supaClient.auth.currentUser != null) {
debugPrint('initial FCM token for signed-in user: $initialToken');
final ctrl = NotificationsController(supaClient);
await ctrl.registerFcmToken(initialToken);
}
} }
// listen for auth changes to register/unregister token accordingly // listen for auth changes to register/unregister token accordingly
supaClient.auth.onAuthStateChange.listen((data) async { supaClient.auth.onAuthStateChange.listen((data) async {
final event = data.event; final event = data.event;
final token = await FirebaseMessaging.instance.getToken(); if (kIsWeb) {
debugPrint(
'auth state change $event on web: skipping FCM token handling',
);
return;
}
String? token;
try {
token = await FirebaseMessaging.instance.getToken();
} catch (e) {
debugPrint('FCM getToken failed during auth change: $e');
token = null;
}
debugPrint('auth state change $event, token=$token'); debugPrint('auth state change $event, token=$token');
if (token == null) return; if (token == null) return;
final ctrl = NotificationsController(supaClient); final ctrl = NotificationsController(supaClient);
@ -77,18 +98,20 @@ Future<void> main() async {
} }
}); });
// on Android 13+ we must request POST_NOTIFICATIONS at runtime; without it if (!kIsWeb) {
// notifications are automatically denied and cannot be reenabled from the // on Android 13+ we must request POST_NOTIFICATIONS at runtime; without it
// system settings. The helper uses `permission_handler`. // notifications are automatically denied and cannot be reenabled from the
final granted = await ensureNotificationPermission(); // system settings. The helper uses `permission_handler`.
if (!granted) { final granted = await ensureNotificationPermission();
// we dont block startup, but its worth logging so developers notice. A if (!granted) {
// real app might show a dialog pointing the user to settings. // we dont block startup, but its worth logging so developers notice.
// debugPrint('notification permission not granted'); // A real app might show a dialog pointing the user to settings.
} // debugPrint('notification permission not granted');
}
// request FCM permission (iOS/Android13+) and handle foreground messages // request FCM permission (iOS/Android13+) and handle foreground messages
await FirebaseMessaging.instance.requestPermission(); await FirebaseMessaging.instance.requestPermission();
}
FirebaseMessaging.onMessage.listen((RemoteMessage message) { FirebaseMessaging.onMessage.listen((RemoteMessage message) {
final notification = message.notification; final notification = message.notification;
if (notification != null) { if (notification != null) {
@ -160,25 +183,35 @@ class NotificationSoundObserver extends ProviderObserver {
if (profile != null) { if (profile != null) {
// signed in: save current token and keep listening for refreshes // signed in: save current token and keep listening for refreshes
FirebaseMessaging.instance.getToken().then((token) { if (!kIsWeb) {
if (token != null) { FirebaseMessaging.instance
debugPrint('profile observer registering token: $token'); .getToken()
.then((token) {
if (token != null) {
debugPrint('profile observer registering token: $token');
controller.registerFcmToken(token);
}
})
.catchError((e) => debugPrint('getToken error: $e'));
_tokenSub = FirebaseMessaging.instance.onTokenRefresh.listen((token) {
debugPrint('token refreshed: $token');
controller.registerFcmToken(token); controller.registerFcmToken(token);
} });
}); }
_tokenSub = FirebaseMessaging.instance.onTokenRefresh.listen((token) {
debugPrint('token refreshed: $token');
controller.registerFcmToken(token);
});
} else { } else {
// signed out: unregister whatever token we currently have // signed out: unregister whatever token we currently have
_tokenSub?.cancel(); _tokenSub?.cancel();
FirebaseMessaging.instance.getToken().then((token) { if (!kIsWeb) {
if (token != null) { FirebaseMessaging.instance
debugPrint('profile observer unregistering token: $token'); .getToken()
controller.unregisterFcmToken(token); .then((token) {
} if (token != null) {
}); debugPrint('profile observer unregistering token: $token');
controller.unregisterFcmToken(token);
}
})
.catchError((e) => debugPrint('getToken error: $e'));
}
} }
} }
} }

View File

@ -1,4 +1,5 @@
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
/// Information about a permission that the application cares about. /// Information about a permission that the application cares about.
/// ///
@ -24,19 +25,34 @@ class PermissionInfo {
/// it should be expanded as new features are added. Declaring a permission /// it should be expanded as new features are added. Declaring a permission
/// here ensures it appears on the permissions screen and is considered when /// here ensures it appears on the permissions screen and is considered when
/// performing bulk checks/requests. /// performing bulk checks/requests.
const List<PermissionInfo> appPermissions = [ List<PermissionInfo> get appPermissions {
PermissionInfo( // On web, browser notifications are handled separately; avoid including
permission: Permission.notification, // the notification runtime permission in the app permissions list so the
label: 'Notifications', // permissions screen and bulk request flow do not attempt to enforce it.
description: 'Deliver alerts and sounds', if (kIsWeb) {
), return [
PermissionInfo( PermissionInfo(
permission: Permission.location, permission: Permission.location,
label: 'Location', label: 'Location',
description: 'Access your location for geofencing', description: 'Access your location for geofencing',
), ),
// add new permissions here as necessary ];
]; }
return [
PermissionInfo(
permission: Permission.notification,
label: 'Notifications',
description: 'Deliver alerts and sounds',
),
PermissionInfo(
permission: Permission.location,
label: 'Location',
description: 'Access your location for geofencing',
),
// add new permissions here as necessary
];
}
/// Returns the current status for every permission in [appPermissions]. /// Returns the current status for every permission in [appPermissions].
Future<Map<Permission, PermissionStatus>> getAllStatuses() async { Future<Map<Permission, PermissionStatus>> getAllStatuses() async {

View File

@ -1,4 +1,5 @@
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import '../services/permission_service.dart'; import '../services/permission_service.dart';
@ -8,10 +9,14 @@ import '../services/permission_service.dart';
/// [permission_service]. /// [permission_service].
Future<PermissionStatus> requestNotificationPermission() async { Future<PermissionStatus> requestNotificationPermission() async {
if (kIsWeb) {
return PermissionStatus.granted;
}
return requestPermission(Permission.notification); return requestPermission(Permission.notification);
} }
Future<bool> ensureNotificationPermission() async { Future<bool> ensureNotificationPermission() async {
if (kIsWeb) return true;
final status = await Permission.notification.status; final status = await Permission.notification.status;
if (status.isGranted) return true; if (status.isGranted) return true;
if (status.isDenied || status.isRestricted || status.isLimited) { if (status.isDenied || status.isRestricted || status.isLimited) {