Skip to content

Commit

Permalink
[flutter_local_notifications] Check permissions for iOS and macOS (#2145
Browse files Browse the repository at this point in the history
)

* Check notifications for iOS 10 and newer

* Use check notification method with plugin

* Check permissions for iOS 9 and below

* Add check for example with iOS only

* Update everything for macOS

* Fixes for provisionalPermission

* Add tests

* Add feature to README

* Remove logs

* Rework for iOS

* Macos, docs, fixes

* Rename fields
  • Loading branch information
Vorkytaka committed Dec 26, 2023
1 parent ca71c96 commit d8b7143
Show file tree
Hide file tree
Showing 9 changed files with 249 additions and 3 deletions.
1 change: 1 addition & 0 deletions flutter_local_notifications/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ A cross platform plugin for displaying local notifications.
* [Android] Ability to check if notifications are enabled
* [iOS (all supported versions) & macOS 10.14+] Request notification permissions and customise the permissions being requested around displaying notifications
* [iOS 10 or newer and macOS 10.14 or newer] Display notifications with attachments
* [iOS and macOS 10.14 or newer] Ability to check if notifications are enabled with specific type check
* [Linux] Ability to to use themed/Flutter Assets icons and sound
* [Linux] Ability to to set the category
* [Linux] Configuring the urgency
Expand Down
42 changes: 41 additions & 1 deletion flutter_local_notifications/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,10 @@ class _HomePageState extends State<HomePage> {
'iOS and macOS-specific examples',
style: TextStyle(fontWeight: FontWeight.bold),
),
PaddedElevatedButton(
buttonText: 'Check permissions',
onPressed: _checkNotificationsOnCupertino,
),
PaddedElevatedButton(
buttonText: 'Request permission',
onPressed: _requestPermissions,
Expand Down Expand Up @@ -2329,6 +2333,41 @@ class _HomePageState extends State<HomePage> {
));
}

Future<void> _checkNotificationsOnCupertino() async {
final NotificationsEnabledOptions? isEnabled =
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin>()
?.checkPermissions() ??
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
MacOSFlutterLocalNotificationsPlugin>()
?.checkPermissions();
final String isEnabledString = isEnabled == null
? 'ERROR: received null'
: '''
isEnabled: ${isEnabled.isEnabled}
isSoundEnabled: ${isEnabled.isSoundEnabled}
isAlertEnabled: ${isEnabled.isAlertEnabled}
isBadgeEnabled: ${isEnabled.isBadgeEnabled}
isProvisionalEnabled: ${isEnabled.isProvisionalEnabled}
isCriticalEnabled: ${isEnabled.isCriticalEnabled}
''';
await showDialog<void>(
context: context,
builder: (BuildContext context) => AlertDialog(
content: Text(isEnabledString),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('OK'),
),
],
));
}

Future<void> _deleteNotificationChannel() async {
const String channelId = 'your channel id 2';
await flutterLocalNotificationsPlugin
Expand Down Expand Up @@ -2682,7 +2721,8 @@ Future<void> _showLinuxNotificationWithByteDataIcon() async {
data: iconBytes,
width: iconData.width,
height: iconData.height,
channels: 4, // The icon has an alpha channel
channels: 4,
// The icon has an alpha channel
hasAlpha: true,
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ @implementation FlutterLocalNotificationsPlugin {
NSString *const ON_NOTIFICATION_METHOD = @"onNotification";
NSString *const DID_RECEIVE_LOCAL_NOTIFICATION = @"didReceiveLocalNotification";
NSString *const REQUEST_PERMISSIONS_METHOD = @"requestPermissions";
NSString *const CHECK_PERMISSIONS_METHOD = @"checkPermissions";

NSString *const DAY = @"day";

Expand Down Expand Up @@ -96,6 +97,13 @@ @implementation FlutterLocalNotificationsPlugin {
NSString *const PRESENTATION_OPTIONS_USER_DEFAULTS =
@"flutter_local_notifications_presentation_options";

NSString *const IS_NOTIFICATIONS_ENABLED = @"isEnabled";
NSString *const IS_SOUND_ENABLED = @"isSoundEnabled";
NSString *const IS_ALERT_ENABLED = @"isAlertEnabled";
NSString *const IS_BADGE_ENABLED = @"isBadgeEnabled";
NSString *const IS_PROVISIONAL_ENABLED = @"isProvisionalEnabled";
NSString *const IS_CRITICAL_ENABLED = @"isCriticalEnabled";

typedef NS_ENUM(NSInteger, RepeatInterval) {
EveryMinute,
Hourly,
Expand Down Expand Up @@ -170,6 +178,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call
[self periodicallyShow:call.arguments result:result];
} else if ([REQUEST_PERMISSIONS_METHOD isEqualToString:call.method]) {
[self requestPermissions:call.arguments result:result];
} else if ([CHECK_PERMISSIONS_METHOD isEqualToString:call.method]) {
[self checkPermissions:call.arguments result:result];
} else if ([CANCEL_METHOD isEqualToString:call.method]) {
[self cancel:((NSNumber *)call.arguments) result:result];
} else if ([CANCEL_ALL_METHOD isEqualToString:call.method]) {
Expand Down Expand Up @@ -545,6 +555,75 @@ - (void)requestPermissionsImpl:(bool)soundPermission
}
}

- (void)checkPermissions:(NSDictionary *_Nonnull)arguments

result:(FlutterResult _Nonnull)result {
if (@available(iOS 10.0, *)) {
UNUserNotificationCenter *center =
[UNUserNotificationCenter currentNotificationCenter];

[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
BOOL isEnabled = settings.authorizationStatus == UNAuthorizationStatusAuthorized;
BOOL isSoundEnabled = settings.soundSetting == UNNotificationSettingEnabled;
BOOL isAlertEnabled = settings.alertSetting == UNNotificationSettingEnabled;
BOOL isBadgeEnabled = settings.badgeSetting == UNNotificationSettingEnabled;
BOOL isProvisionalEnabled = false;
BOOL isCriticalEnabled = false;

if(@available(iOS 12.0, *)) {
isProvisionalEnabled = settings.authorizationStatus == UNAuthorizationStatusProvisional;
isCriticalEnabled = settings.criticalAlertSetting == UNNotificationSettingEnabled;
}

NSDictionary *dict = @{
IS_NOTIFICATIONS_ENABLED: @(isEnabled),
IS_SOUND_ENABLED: @(isSoundEnabled),
IS_ALERT_ENABLED: @(isAlertEnabled),
IS_BADGE_ENABLED: @(isBadgeEnabled),
IS_PROVISIONAL_ENABLED: @(isProvisionalEnabled),
IS_CRITICAL_ENABLED: @(isCriticalEnabled),
};

result(dict);
}];
} else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
UIUserNotificationSettings *settings = UIApplication.sharedApplication.currentUserNotificationSettings;

if(settings == nil) {
result(@{
IS_NOTIFICATIONS_ENABLED: @NO,
IS_SOUND_ENABLED: @NO,
IS_ALERT_ENABLED: @NO,
IS_BADGE_ENABLED: @NO,
IS_PROVISIONAL_ENABLED: @NO,
IS_CRITICAL_ENABLED: @NO,
});
return;
}

UIUserNotificationType types = settings.types;

BOOL isEnabled = types != UIUserNotificationTypeNone;
BOOL isSoundEnabled = types & UIUserNotificationTypeSound;
BOOL isAlertEnabled = types & UIUserNotificationTypeAlert;
BOOL isBadgeEnabled = types & UIUserNotificationTypeBadge;

NSDictionary *dict = @{
IS_NOTIFICATIONS_ENABLED: @(isEnabled),
IS_SOUND_ENABLED: @(isSoundEnabled),
IS_ALERT_ENABLED: @(isAlertEnabled),
IS_BADGE_ENABLED: @(isBadgeEnabled),
IS_PROVISIONAL_ENABLED: @NO,
IS_CRITICAL_ENABLED: @NO,
};

result(dict);
#pragma clang diagnostic pop
}
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
- (UILocalNotification *)buildStandardUILocalNotification:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export 'src/platform_specifics/darwin/notification_attachment.dart';
export 'src/platform_specifics/darwin/notification_category.dart';
export 'src/platform_specifics/darwin/notification_category_option.dart';
export 'src/platform_specifics/darwin/notification_details.dart';
export 'src/platform_specifics/darwin/notification_enabled_options.dart';
export 'src/platform_specifics/ios/enums.dart';
export 'src/typedefs.dart';
export 'src/types.dart';
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import 'platform_specifics/android/styles/messaging_style_information.dart';
import 'platform_specifics/darwin/initialization_settings.dart';
import 'platform_specifics/darwin/mappers.dart';
import 'platform_specifics/darwin/notification_details.dart';
import 'platform_specifics/darwin/notification_enabled_options.dart';
import 'platform_specifics/ios/enums.dart';
import 'typedefs.dart';
import 'types.dart';
Expand Down Expand Up @@ -616,6 +617,27 @@ class IOSFlutterLocalNotificationsPlugin
'critical': critical,
});

/// Returns whether the app can post notifications and what kind of.
///
/// See [NotificationsEnabledOptions] for more info.
Future<NotificationsEnabledOptions?> checkPermissions() =>
_channel.invokeMethod<Map<dynamic, dynamic>?>('checkPermissions').then(
(Map<dynamic, dynamic>? dict) {
if (dict == null) {
return null;
}

return NotificationsEnabledOptions(
isEnabled: dict['isEnabled'] ?? false,
isAlertEnabled: dict['isAlertEnabled'] ?? false,
isBadgeEnabled: dict['isBadgeEnabled'] ?? false,
isSoundEnabled: dict['isSoundEnabled'] ?? false,
isProvisionalEnabled: dict['isProvisionalEnabled'] ?? false,
isCriticalEnabled: dict['isCriticalEnabled'] ?? false,
);
},
);

/// Schedules a notification to be shown at the specified time in the
/// future in a specific time zone.
///
Expand Down Expand Up @@ -785,6 +807,26 @@ class MacOSFlutterLocalNotificationsPlugin
'critical': critical,
});

/// Returns whether the app can post notifications and what kind of.
///
/// See [NotificationsEnabledOptions] for more info.
Future<NotificationsEnabledOptions?> checkPermissions() => _channel
.invokeMethod<Map<dynamic, dynamic>>('checkPermissions')
.then((Map<dynamic, dynamic>? dict) {
if (dict == null) {
return null;
}

return NotificationsEnabledOptions(
isEnabled: dict['isEnabled'] ?? false,
isAlertEnabled: dict['isAlertEnabled'] ?? false,
isBadgeEnabled: dict['isBadgeEnabled'] ?? false,
isSoundEnabled: dict['isSoundEnabled'] ?? false,
isProvisionalEnabled: dict['isProvisionalEnabled'] ?? false,
isCriticalEnabled: dict['isCriticalEnabled'] ?? false,
);
});

/// Schedules a notification to be shown at the specified date and time
/// relative to a specific time zone.
Future<void> zonedSchedule(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/// Data class that represent current state of notification options.
///
/// Used for Darwin systems, like iOS and macOS.
class NotificationsEnabledOptions {
/// Constructs an instance of [NotificationsEnabledOptions]
const NotificationsEnabledOptions({
required this.isEnabled,
required this.isSoundEnabled,
required this.isAlertEnabled,
required this.isBadgeEnabled,
required this.isProvisionalEnabled,
required this.isCriticalEnabled,
});

/// Whenever notifications are enabled.
///
/// Can be either [isEnabled] or [isProvisionalEnabled].
final bool isEnabled;

/// Whenever sound notifications are enabled.
final bool isSoundEnabled;

/// Whenever alert notifications are enabled.
final bool isAlertEnabled;

/// Whenever badge notifications are enabled.
final bool isBadgeEnabled;

/// Whenever provisional notifications are enabled.
///
/// Can be either [isEnabled] or [isProvisionalEnabled].
final bool isProvisionalEnabled;

/// Whenever critical notifications are enabled.
final bool isCriticalEnabled;
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot
static let interruptionLevel = "interruptionLevel"
static let actionId = "actionId"
static let notificationResponseType = "notificationResponseType"
static let isNotificationsEnabled = "isEnabled"
static let isSoundEnabled = "isSoundEnabled"
static let isAlertEnabled = "isAlertEnabled"
static let isBadgeEnabled = "isBadgeEnabled"
static let isProvisionalEnabled = "isProvisionalEnabled"
static let isCriticalEnabled = "isCriticalEnabled"
}

struct ErrorMessages {
Expand Down Expand Up @@ -193,6 +199,8 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot
initialize(call, result)
case "requestPermissions":
requestPermissions(call, result)
case "checkPermissions":
checkPermissions(call, result)
case "getNotificationAppLaunchDetails":
getNotificationAppLaunchDetails(result)
case "cancel":
Expand Down Expand Up @@ -665,6 +673,25 @@ public class FlutterLocalNotificationsPlugin: NSObject, FlutterPlugin, UNUserNot
result(granted)
}
}

func checkPermissions(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
if #available(macOS 10.14, *) {
UNUserNotificationCenter.current().getNotificationSettings { settings in
let dict = [
MethodCallArguments.isNotificationsEnabled: settings.authorizationStatus == .authorized,
MethodCallArguments.isSoundEnabled: settings.soundSetting == .enabled,
MethodCallArguments.isAlertEnabled: settings.alertSetting == .enabled,
MethodCallArguments.isBadgeEnabled: settings.badgeSetting == .enabled,
MethodCallArguments.isProvisionalEnabled: settings.authorizationStatus == .provisional,
MethodCallArguments.isCriticalEnabled: settings.criticalAlertSetting == .enabled,
]

result(dict)
}
} else {
result(nil)
}
}

@available(macOS, introduced: 10.8, deprecated: 11.0)
func buildNSUserNotification(fromArguments arguments: [String: AnyObject]) -> NSUserNotification {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ void main() {
setUp(() {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
TestDefaultBinaryMessengerBinding.instance?.defaultBinaryMessenger

Check warning on line 23 in flutter_local_notifications/test/ios_flutter_local_notifications_test.dart

View workflow job for this annotation

GitHub Actions / Run Dart Analyzer

The receiver can't be null, so the null-aware operator '?.' is unnecessary.

Try replacing the operator '?.' with '.'. See https://dart.dev/diagnostics/invalid_null_aware_operator to learn more about this problem.
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
log.add(methodCall);
if (methodCall.method == 'pendingNotificationRequests') {
Expand Down Expand Up @@ -606,6 +606,15 @@ void main() {
})
]);
});

test('checkPermissions', () async {
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin>()!
.checkPermissions();
expect(log, <Matcher>[isMethodCall('checkPermissions', arguments: null)]);
});

test('cancel', () async {
await flutterLocalNotificationsPlugin.cancel(1);
expect(log, <Matcher>[isMethodCall('cancel', arguments: 1)]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ void main() {
setUp(() {
debugDefaultTargetPlatformOverride = TargetPlatform.macOS;
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
TestDefaultBinaryMessengerBinding.instance?.defaultBinaryMessenger

Check warning on line 22 in flutter_local_notifications/test/macos_flutter_local_notifications_test.dart

View workflow job for this annotation

GitHub Actions / Run Dart Analyzer

The receiver can't be null, so the null-aware operator '?.' is unnecessary.

Try replacing the operator '?.' with '.'. See https://dart.dev/diagnostics/invalid_null_aware_operator to learn more about this problem.
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
log.add(methodCall);
if (methodCall.method == 'pendingNotificationRequests') {
Expand Down Expand Up @@ -522,6 +522,17 @@ void main() {
})
]);
});

test('checkPermissions', () async {
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
MacOSFlutterLocalNotificationsPlugin>()!
.checkPermissions();
expect(log, <Matcher>[
isMethodCall('checkPermissions', arguments: null)
]);
});

test('cancel', () async {
await flutterLocalNotificationsPlugin.cancel(1);
expect(log, <Matcher>[isMethodCall('cancel', arguments: 1)]);
Expand Down

0 comments on commit d8b7143

Please sign in to comment.