From aef88de54b919db03382fa002e5dfac53386b372 Mon Sep 17 00:00:00 2001 From: Marc Rejohn Castillano Date: Wed, 25 Feb 2026 18:39:39 +0800 Subject: [PATCH] No notification permission enforcement on web --- lib/main.dart | 97 +++++++++++++++++--------- lib/services/permission_service.dart | 42 +++++++---- lib/utils/notification_permission.dart | 5 ++ 3 files changed, 99 insertions(+), 45 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 6c6b601e..4945c274 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:audioplayers/audioplayers.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -56,17 +57,37 @@ Future main() async { // ensure token saved right away if already signed in final supaClient = Supabase.instance.client; - final initialToken = await FirebaseMessaging.instance.getToken(); - if (initialToken != null && supaClient.auth.currentUser != null) { - debugPrint('initial FCM token for signed-in user: $initialToken'); - final ctrl = NotificationsController(supaClient); - await ctrl.registerFcmToken(initialToken); + String? initialToken; + if (!kIsWeb) { + try { + initialToken = await FirebaseMessaging.instance.getToken(); + } 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 supaClient.auth.onAuthStateChange.listen((data) async { 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'); if (token == null) return; final ctrl = NotificationsController(supaClient); @@ -77,18 +98,20 @@ Future main() async { } }); - // on Android 13+ we must request POST_NOTIFICATIONS at runtime; without it - // notifications are automatically denied and cannot be re‑enabled from the - // system settings. The helper uses `permission_handler`. - final granted = await ensureNotificationPermission(); - if (!granted) { - // we don’t block startup, but it’s worth logging so developers notice. A - // real app might show a dialog pointing the user to settings. - // debugPrint('notification permission not granted'); - } + if (!kIsWeb) { + // on Android 13+ we must request POST_NOTIFICATIONS at runtime; without it + // notifications are automatically denied and cannot be re‑enabled from the + // system settings. The helper uses `permission_handler`. + final granted = await ensureNotificationPermission(); + if (!granted) { + // we don’t block startup, but it’s worth logging so developers notice. + // 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 - await FirebaseMessaging.instance.requestPermission(); + // request FCM permission (iOS/Android13+) and handle foreground messages + await FirebaseMessaging.instance.requestPermission(); + } FirebaseMessaging.onMessage.listen((RemoteMessage message) { final notification = message.notification; if (notification != null) { @@ -160,25 +183,35 @@ class NotificationSoundObserver extends ProviderObserver { if (profile != null) { // signed in: save current token and keep listening for refreshes - FirebaseMessaging.instance.getToken().then((token) { - if (token != null) { - debugPrint('profile observer registering token: $token'); + if (!kIsWeb) { + FirebaseMessaging.instance + .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); - } - }); - _tokenSub = FirebaseMessaging.instance.onTokenRefresh.listen((token) { - debugPrint('token refreshed: $token'); - controller.registerFcmToken(token); - }); + }); + } } else { // signed out: unregister whatever token we currently have _tokenSub?.cancel(); - FirebaseMessaging.instance.getToken().then((token) { - if (token != null) { - debugPrint('profile observer unregistering token: $token'); - controller.unregisterFcmToken(token); - } - }); + if (!kIsWeb) { + FirebaseMessaging.instance + .getToken() + .then((token) { + if (token != null) { + debugPrint('profile observer unregistering token: $token'); + controller.unregisterFcmToken(token); + } + }) + .catchError((e) => debugPrint('getToken error: $e')); + } } } } diff --git a/lib/services/permission_service.dart b/lib/services/permission_service.dart index 721a3b6a..e5818923 100644 --- a/lib/services/permission_service.dart +++ b/lib/services/permission_service.dart @@ -1,4 +1,5 @@ import 'package:permission_handler/permission_handler.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; /// 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 /// here ensures it appears on the permissions screen and is considered when /// performing bulk checks/requests. -const List appPermissions = [ - 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 -]; +List get appPermissions { + // On web, browser notifications are handled separately; avoid including + // the notification runtime permission in the app permissions list so the + // permissions screen and bulk request flow do not attempt to enforce it. + if (kIsWeb) { + return [ + PermissionInfo( + permission: Permission.location, + label: 'Location', + description: 'Access your location for geofencing', + ), + ]; + } + + 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]. Future> getAllStatuses() async { diff --git a/lib/utils/notification_permission.dart b/lib/utils/notification_permission.dart index c3de8623..704357e7 100644 --- a/lib/utils/notification_permission.dart +++ b/lib/utils/notification_permission.dart @@ -1,4 +1,5 @@ import 'package:permission_handler/permission_handler.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; import '../services/permission_service.dart'; @@ -8,10 +9,14 @@ import '../services/permission_service.dart'; /// [permission_service]. Future requestNotificationPermission() async { + if (kIsWeb) { + return PermissionStatus.granted; + } return requestPermission(Permission.notification); } Future ensureNotificationPermission() async { + if (kIsWeb) return true; final status = await Permission.notification.status; if (status.isGranted) return true; if (status.isDenied || status.isRestricted || status.isLimited) {