diff --git a/flutter_local_notifications/README.md b/flutter_local_notifications/README.md index f0b500b84..4c8f94896 100644 --- a/flutter_local_notifications/README.md +++ b/flutter_local_notifications/README.md @@ -101,6 +101,7 @@ Note: the plugin requires Flutter SDK 3.22 at a minimum. The list of support pla * [Android] Start a foreground service * [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+] Request CarPlay notification permissions for notifications to appear in CarPlay interface * [iOS 10 or newer and macOS 10.14 or newer] Display notifications with attachments * [iOS 12.0+] Support for custom notification settings UI via "Configure Notifications in " button in notification context menu (API available on macOS 10.14+ but UI button does not appear in practice) * [iOS and macOS 10.14 or newer] Ability to check if notifications are enabled with specific type check @@ -674,6 +675,62 @@ Initialisation can be done in the `main` function of your application or can be Note that all settings are nullable, because we don't want to force developers so specify settings for platforms they don't target. You will get a runtime ArgumentError Exception if you forgot to pass the settings for the platform you target. +### [iOS-specific] Using IOSInitializationSettings + +Starting from version 17.3.0, iOS developers can use the more specific `IOSInitializationSettings` class instead of `DarwinInitializationSettings` to access iOS-only features like CarPlay notification permissions. The `IOSInitializationSettings` class extends `DarwinInitializationSettings`, so all existing Darwin features are still available. + +```dart +const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('app_icon'); +final IOSInitializationSettings initializationSettingsIOS = + IOSInitializationSettings( + // Darwin settings + requestAlertPermission: true, + requestBadgePermission: true, + requestSoundPermission: true, + // iOS-specific settings + requestCarPlayPermission: true, + ); +final DarwinInitializationSettings initializationSettingsDarwin = + DarwinInitializationSettings(); +final InitializationSettings initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsIOS, // Use iOS-specific settings + macOS: initializationSettingsDarwin, // Keep Darwin for macOS +); +``` + +#### CarPlay Integration + +When `requestCarPlayPermission` is set to `true`, the plugin will request CarPlay notification permissions if the device supports it (iOS 10.0+). This allows your app's notifications to appear on the CarPlay interface when the device is connected to a compatible vehicle. + +**Important considerations:** +- CarPlay permissions are only available on iOS 10.0 and later +- The permission request will be silently ignored on unsupported devices +- CarPlay notifications follow the same content guidelines as regular iOS notifications +- Test CarPlay integration using the iOS Simulator's CarPlay mode + +#### Migration from DarwinInitializationSettings + +Existing code using `DarwinInitializationSettings` for iOS will continue to work without changes. To migrate to iOS-specific features: + +1. Replace `DarwinInitializationSettings` with `IOSInitializationSettings` in your iOS initialization +2. Add any iOS-specific options like `requestCarPlayPermission` +3. Keep using `DarwinInitializationSettings` for macOS initialization + +```dart +// Before (still works) +final DarwinInitializationSettings initializationSettingsDarwin = + DarwinInitializationSettings(requestAlertPermission: true); + +// After (with iOS-specific features) +final IOSInitializationSettings initializationSettingsIOS = + IOSInitializationSettings( + requestAlertPermission: true, + requestCarPlayPermission: true, + ); +``` + ```dart void onDidReceiveNotificationResponse(NotificationResponse notificationResponse) async { final String? payload = notificationResponse.payload; diff --git a/flutter_local_notifications/example/integration_test/flutter_local_notifications_test.dart b/flutter_local_notifications/example/integration_test/flutter_local_notifications_test.dart index a6b83b525..508cd8bf0 100644 --- a/flutter_local_notifications/example/integration_test/flutter_local_notifications_test.dart +++ b/flutter_local_notifications/example/integration_test/flutter_local_notifications_test.dart @@ -8,8 +8,8 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('app_icon'); - const DarwinInitializationSettings initializationSettingsIOS = - DarwinInitializationSettings(); + const IOSInitializationSettings initializationSettingsIOS = + IOSInitializationSettings(); const DarwinInitializationSettings initializationSettingsMacOS = DarwinInitializationSettings(); final LinuxInitializationSettings initializationSettingsLinux = diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 011653a2b..eaadc3b33 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -136,7 +136,14 @@ Future main() async { /// Note: permissions aren't requested here just to demonstrate that can be /// done later - final DarwinInitializationSettings initializationSettingsDarwin = + final IOSInitializationSettings initializationSettingsIOS = + IOSInitializationSettings( + requestAlertPermission: false, + requestBadgePermission: false, + requestSoundPermission: false, + notificationCategories: darwinNotificationCategories, + ); + final DarwinInitializationSettings initializationSettingsMacOS = DarwinInitializationSettings( requestAlertPermission: false, requestBadgePermission: false, @@ -152,8 +159,8 @@ Future main() async { final InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, - iOS: initializationSettingsDarwin, - macOS: initializationSettingsDarwin, + iOS: initializationSettingsIOS, + macOS: initializationSettingsMacOS, linux: initializationSettingsLinux, windows: windows.initSettings, ); diff --git a/flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/FlutterLocalNotificationsPlugin.m b/flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/FlutterLocalNotificationsPlugin.m index 3bca2ee6d..cc1881d8c 100644 --- a/flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/FlutterLocalNotificationsPlugin.m +++ b/flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/FlutterLocalNotificationsPlugin.m @@ -49,6 +49,7 @@ @implementation FlutterLocalNotificationsPlugin { NSString *const REQUEST_PROVISIONAL_PERMISSION = @"requestProvisionalPermission"; NSString *const REQUEST_CRITICAL_PERMISSION = @"requestCriticalPermission"; +NSString *const REQUEST_CARPLAY_PERMISSION = @"requestCarPlayPermission"; NSString *const REQUEST_PROVIDES_APP_NOTIFICATION_SETTINGS = @"requestProvidesAppNotificationSettings"; NSString *const DEFAULT_PRESENT_ALERT = @"defaultPresentAlert"; @@ -61,6 +62,7 @@ @implementation FlutterLocalNotificationsPlugin { NSString *const BADGE_PERMISSION = @"badge"; NSString *const PROVISIONAL_PERMISSION = @"provisional"; NSString *const CRITICAL_PERMISSION = @"critical"; +NSString *const CARPLAY_PERMISSION = @"carPlay"; NSString *const PROVIDES_APP_NOTIFICATION_SETTINGS = @"providesAppNotificationSettings"; NSString *const CALLBACK_DISPATCHER = @"callbackDispatcher"; @@ -112,6 +114,7 @@ @implementation FlutterLocalNotificationsPlugin { NSString *const IS_CRITICAL_ENABLED = @"isCriticalEnabled"; NSString *const IS_PROVIDES_APP_NOTIFICATION_SETTINGS_ENABLED = @"isProvidesAppNotificationSettingsEnabled"; +NSString *const IS_CAR_PLAY_ENABLED = @"isCarPlayEnabled"; NSString *const CRITICAL_SOUND_VOLUME = @"criticalSoundVolume"; @@ -353,6 +356,8 @@ - (void)initialize:(NSDictionary *_Nonnull)arguments bool requestedBadgePermission = false; bool requestedProvisionalPermission = false; bool requestedCriticalPermission = false; + bool requestedCarPlayPermission = false; + bool carPlayPermissionSpecified = false; bool requestedProvidesAppNotificationSettings = false; NSMutableDictionary *presentationOptions = [[NSMutableDictionary alloc] init]; if ([self containsKey:DEFAULT_PRESENT_ALERT forDictionary:arguments]) { @@ -401,6 +406,11 @@ - (void)initialize:(NSDictionary *_Nonnull)arguments requestedCriticalPermission = [arguments[REQUEST_CRITICAL_PERMISSION] boolValue]; } + if ([self containsKey:REQUEST_CARPLAY_PERMISSION forDictionary:arguments]) { + carPlayPermissionSpecified = true; + requestedCarPlayPermission = + [arguments[REQUEST_CARPLAY_PERMISSION] boolValue]; + } if ([self containsKey:REQUEST_PROVIDES_APP_NOTIFICATION_SETTINGS forDictionary:arguments]) { requestedProvidesAppNotificationSettings = @@ -426,6 +436,8 @@ - (void)initialize:(NSDictionary *_Nonnull)arguments requestedProvisionalPermission criticalPermission: requestedCriticalPermission + carPlayPermission:requestedCarPlayPermission + carPlayPermissionSpecified:carPlayPermissionSpecified providesAppNotificationSettings: requestedProvidesAppNotificationSettings result:result]; @@ -440,6 +452,8 @@ - (void)requestPermissions:(NSDictionary *_Nonnull)arguments bool badgePermission = false; bool provisionalPermission = false; bool criticalPermission = false; + bool requestCarPlayPermission = false; + bool carPlayPermissionSpecified = false; bool providesAppNotificationSettings = false; if ([self containsKey:SOUND_PERMISSION forDictionary:arguments]) { soundPermission = [arguments[SOUND_PERMISSION] boolValue]; @@ -456,6 +470,12 @@ - (void)requestPermissions:(NSDictionary *_Nonnull)arguments if ([self containsKey:CRITICAL_PERMISSION forDictionary:arguments]) { criticalPermission = [arguments[CRITICAL_PERMISSION] boolValue]; } + if ([self containsKey:CARPLAY_PERMISSION forDictionary:arguments]) { + carPlayPermissionSpecified = true; + requestCarPlayPermission = + [arguments[CARPLAY_PERMISSION] boolValue]; + + } if ([self containsKey:PROVIDES_APP_NOTIFICATION_SETTINGS forDictionary:arguments]) { providesAppNotificationSettings = @@ -466,6 +486,8 @@ - (void)requestPermissions:(NSDictionary *_Nonnull)arguments badgePermission:badgePermission provisionalPermission:provisionalPermission criticalPermission:criticalPermission + carPlayPermission:requestCarPlayPermission + carPlayPermissionSpecified:carPlayPermissionSpecified providesAppNotificationSettings:providesAppNotificationSettings result:result]; } @@ -475,6 +497,8 @@ - (void)requestPermissionsImpl:(bool)soundPermission badgePermission:(bool)badgePermission provisionalPermission:(bool)provisionalPermission criticalPermission:(bool)criticalPermission + carPlayPermission:(bool)carPlayPermission + carPlayPermissionSpecified:(bool)carPlayPermissionSpecified providesAppNotificationSettings:(bool)providesAppNotificationSettings result:(FlutterResult _Nonnull)result { if (!soundPermission && !alertPermission && !badgePermission && @@ -495,6 +519,11 @@ - (void)requestPermissionsImpl:(bool)soundPermission if (badgePermission) { authorizationOptions += UNAuthorizationOptionBadge; } + if (@available(iOS 10.0, *)) { + if (carPlayPermissionSpecified && carPlayPermission) { + authorizationOptions += UNAuthorizationOptionCarPlay; + } + } if (@available(iOS 12.0, *)) { if (provisionalPermission) { authorizationOptions += UNAuthorizationOptionProvisional; @@ -530,6 +559,7 @@ - (void)checkPermissions:(NSDictionary *_Nonnull)arguments BOOL isProvisionalEnabled = false; BOOL isCriticalEnabled = false; BOOL isProvidesAppNotificationSettingsEnabled = false; + BOOL isCarPlayEnabled = false; if (@available(iOS 12.0, *)) { isProvisionalEnabled = @@ -540,6 +570,11 @@ - (void)checkPermissions:(NSDictionary *_Nonnull)arguments settings.providesAppNotificationSettings; } + if (@available(iOS 10.0, *)) { + isCarPlayEnabled = + settings.carPlaySetting == UNNotificationSettingEnabled; + } + NSDictionary *dict = @{ IS_NOTIFICATIONS_ENABLED : @(isEnabled), IS_SOUND_ENABLED : @(isSoundEnabled), @@ -549,6 +584,7 @@ - (void)checkPermissions:(NSDictionary *_Nonnull)arguments IS_CRITICAL_ENABLED : @(isCriticalEnabled), IS_PROVIDES_APP_NOTIFICATION_SETTINGS_ENABLED : @(isProvidesAppNotificationSettingsEnabled), + IS_CAR_PLAY_ENABLED : @(isCarPlayEnabled), }; result(dict); diff --git a/flutter_local_notifications/lib/src/initialization_settings.dart b/flutter_local_notifications/lib/src/initialization_settings.dart index ddd7b356d..f9c414dc5 100644 --- a/flutter_local_notifications/lib/src/initialization_settings.dart +++ b/flutter_local_notifications/lib/src/initialization_settings.dart @@ -21,6 +21,10 @@ class InitializationSettings { /// /// It is nullable, because we don't want to force users to specify settings /// for platforms that they don't target. + /// + /// You can pass either DarwinInitializationSettings or + /// IOSInitializationSettings. Use IOSInitializationSettings to access + /// iOS-specific features like CarPlay. final DarwinInitializationSettings? iOS; /// Settings for macOS. diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index 5fd1bba90..018d01425 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -648,10 +648,14 @@ class IOSFlutterLocalNotificationsPlugin DidReceiveNotificationResponseCallback? _onDidReceiveNotificationResponse; - /// Initializes the plugin. + /// Initializes the plugin for iOS. /// /// Call this method on application before using the plugin further. /// + /// Accepts either [DarwinInitializationSettings] for basic Darwin-based + /// configuration or [IOSInitializationSettings] for iOS-specific features + /// like CarPlay notifications. + /// /// Initialisation may also request notification permissions where users will /// see a permissions prompt. This may be fine in cases where it's acceptable /// to do this when the application runs for the first time. However, if your @@ -664,6 +668,11 @@ class IOSFlutterLocalNotificationsPlugin /// [requestPermissions] can then be called to request permissions when /// needed. /// + /// When using [IOSInitializationSettings], CarPlay notifications can be + /// enabled by setting [IOSInitializationSettings.requestCarPlayPermission] + /// to true. When using [DarwinInitializationSettings], CarPlay is disabled + /// by default. + /// /// The [onDidReceiveNotificationResponse] callback is fired when the user /// selects a notification or notification action that should show the /// application/user interface. @@ -684,7 +693,17 @@ class IOSFlutterLocalNotificationsPlugin _onDidReceiveNotificationResponse = onDidReceiveNotificationResponse; _channel.setMethodCallHandler(_handleMethod); - final Map arguments = initializationSettings.toMap(); + // Convert to map using appropriate mapper based on runtime type + // IOSInitializationSettings.toMap() automatically includes CarPlay field + // DarwinInitializationSettings.toMap() does not include CarPlay field + final Map arguments; + if (initializationSettings is IOSInitializationSettings) { + // Explicitly call iOS mapper extension + arguments = (initializationSettings as IOSInitializationSettings).toMap(); + } else { + // Use Darwin mapper for DarwinInitializationSettings + arguments = initializationSettings.toMap(); + } _evaluateBackgroundNotificationCallback( onDidReceiveBackgroundNotificationResponse, arguments); @@ -694,12 +713,17 @@ class IOSFlutterLocalNotificationsPlugin /// Requests the specified permission(s) from user and returns current /// permission status. + /// + /// On iOS, the [carPlay] parameter requests permission to show notifications + /// on CarPlay when connected to a compatible vehicle. This requires iOS 10.0+ + /// and is only applicable to iOS devices. Future requestPermissions({ bool sound = false, bool alert = false, bool badge = false, bool provisional = false, bool critical = false, + bool carPlay = false, bool providesAppNotificationSettings = false, }) => _channel.invokeMethod('requestPermissions', { @@ -708,6 +732,7 @@ class IOSFlutterLocalNotificationsPlugin 'badge': badge, 'provisional': provisional, 'critical': critical, + 'carPlay': carPlay, 'providesAppNotificationSettings': providesAppNotificationSettings, }); @@ -730,6 +755,7 @@ class IOSFlutterLocalNotificationsPlugin isCriticalEnabled: dict['isCriticalEnabled'] ?? false, isProvidesAppNotificationSettingsEnabled: dict['isProvidesAppNotificationSettingsEnabled'] ?? false, + isCarPlayEnabled: dict['isCarPlayEnabled'] ?? false, ); }, ); diff --git a/flutter_local_notifications/lib/src/platform_specifics/darwin/initialization_settings.dart b/flutter_local_notifications/lib/src/platform_specifics/darwin/initialization_settings.dart index 55a728ce2..2d6814881 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/darwin/initialization_settings.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/darwin/initialization_settings.dart @@ -160,3 +160,34 @@ class DarwinInitializationSettings { /// On macOS, this is only applicable to macOS 10.14 or newer. final List notificationCategories; } + +/// Plugin initialization settings for iOS +class IOSInitializationSettings extends DarwinInitializationSettings { + /// Constructs an instance of [IOSInitializationSettings]. + const IOSInitializationSettings({ + super.requestAlertPermission = true, + super.requestSoundPermission = true, + super.requestBadgePermission = true, + super.requestProvisionalPermission = false, + super.requestCriticalPermission = false, + super.requestProvidesAppNotificationSettings = false, + this.requestCarPlayPermission = false, + super.defaultPresentAlert = true, + super.defaultPresentSound = true, + super.defaultPresentBadge = true, + super.defaultPresentBanner = true, + super.defaultPresentList = true, + super.notificationCategories = const [], + }); + + /// Request permission to show notifications on CarPlay. + /// + /// This permission allows the app to display notifications on the CarPlay + /// interface when connected to a compatible vehicle. + /// + /// Default value is false. + /// + /// This property is only applicable to iOS 10.0 or newer and is iOS-only. + /// CarPlay is not available on macOS. + final bool requestCarPlayPermission; +} diff --git a/flutter_local_notifications/lib/src/platform_specifics/darwin/mappers.dart b/flutter_local_notifications/lib/src/platform_specifics/darwin/mappers.dart index 687d9523d..1650d0164 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/darwin/mappers.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/darwin/mappers.dart @@ -51,6 +51,16 @@ extension DarwinInitializationSettingsMapper on DarwinInitializationSettings { }; } +extension IOSInitializationSettingsMapper on IOSInitializationSettings { + Map toMap() { + final DarwinInitializationSettings darwinSettings = + this as DarwinInitializationSettings; + final Map map = darwinSettings.toMap(); + map['requestCarPlayPermission'] = requestCarPlayPermission; + return map; + } +} + extension on DarwinNotificationAttachmentThumbnailClippingRect { Map toMap() => { 'x': x, diff --git a/flutter_local_notifications/lib/src/platform_specifics/darwin/notification_enabled_options.dart b/flutter_local_notifications/lib/src/platform_specifics/darwin/notification_enabled_options.dart index 8b83e8af6..194f290c1 100644 --- a/flutter_local_notifications/lib/src/platform_specifics/darwin/notification_enabled_options.dart +++ b/flutter_local_notifications/lib/src/platform_specifics/darwin/notification_enabled_options.dart @@ -11,6 +11,7 @@ class NotificationsEnabledOptions { required this.isProvisionalEnabled, required this.isCriticalEnabled, required this.isProvidesAppNotificationSettingsEnabled, + this.isCarPlayEnabled = false, }); /// Whenever notifications are enabled. @@ -44,4 +45,9 @@ class NotificationsEnabledOptions { /// On macOS, the permission can be requested and this value reports /// correctly, but the UI button does not appear in practice. final bool isProvidesAppNotificationSettingsEnabled; + + /// Whether CarPlay notifications are enabled. + /// + /// Available on iOS 10.0 and later. + final bool isCarPlayEnabled; } diff --git a/flutter_local_notifications/test/ios_flutter_local_notifications_test.dart b/flutter_local_notifications/test/ios_flutter_local_notifications_test.dart index f6e66db05..fe1678a48 100644 --- a/flutter_local_notifications/test/ios_flutter_local_notifications_test.dart +++ b/flutter_local_notifications/test/ios_flutter_local_notifications_test.dart @@ -40,8 +40,8 @@ void main() { }); test('initialize with default parameter values', () async { - const DarwinInitializationSettings iosInitializationSettings = - DarwinInitializationSettings(); + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings(); const InitializationSettings initializationSettings = InitializationSettings(iOS: iosInitializationSettings); await flutterLocalNotificationsPlugin.initialize(initializationSettings); @@ -52,6 +52,7 @@ void main() { 'requestBadgePermission': true, 'requestProvisionalPermission': false, 'requestCriticalPermission': false, + 'requestCarPlayPermission': false, 'requestProvidesAppNotificationSettings': false, 'defaultPresentAlert': true, 'defaultPresentSound': true, @@ -64,8 +65,8 @@ void main() { }); test('initialize with notification categories', () async { - final DarwinInitializationSettings iosInitializationSettings = - DarwinInitializationSettings( + final IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings( notificationCategories: [ DarwinNotificationCategory( 'category1', @@ -112,6 +113,7 @@ void main() { 'requestBadgePermission': true, 'requestProvisionalPermission': false, 'requestCriticalPermission': false, + 'requestCarPlayPermission': false, 'requestProvidesAppNotificationSettings': false, 'defaultPresentAlert': true, 'defaultPresentSound': true, @@ -168,8 +170,8 @@ void main() { ]); }); test('initialize with all settings off', () async { - const DarwinInitializationSettings iosInitializationSettings = - DarwinInitializationSettings( + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings( requestAlertPermission: false, requestBadgePermission: false, requestSoundPermission: false, @@ -189,6 +191,7 @@ void main() { 'requestBadgePermission': false, 'requestProvisionalPermission': false, 'requestCriticalPermission': false, + 'requestCarPlayPermission': false, 'requestProvidesAppNotificationSettings': false, 'defaultPresentAlert': false, 'defaultPresentSound': false, @@ -200,9 +203,80 @@ void main() { ]); }); - test('show without iOS-specific details', () async { - const DarwinInitializationSettings iosInitializationSettings = + test('initialize with CarPlay permission enabled', () async { + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings( + requestCarPlayPermission: true, + ); + const InitializationSettings initializationSettings = + InitializationSettings(iOS: iosInitializationSettings); + await flutterLocalNotificationsPlugin.initialize(initializationSettings); + expect(log, [ + isMethodCall('initialize', arguments: { + 'requestAlertPermission': true, + 'requestSoundPermission': true, + 'requestBadgePermission': true, + 'requestProvisionalPermission': false, + 'requestCriticalPermission': false, + 'requestCarPlayPermission': true, + 'requestProvidesAppNotificationSettings': false, + 'defaultPresentAlert': true, + 'defaultPresentSound': true, + 'defaultPresentBadge': true, + 'defaultPresentBanner': true, + 'defaultPresentList': true, + 'notificationCategories': [], + }) + ]); + }); + + test('initialize with DarwinInitializationSettings excludes CarPlay', () async { + const DarwinInitializationSettings darwinInitializationSettings = DarwinInitializationSettings(); + const InitializationSettings initializationSettings = + InitializationSettings(iOS: darwinInitializationSettings); + await flutterLocalNotificationsPlugin.initialize(initializationSettings); + expect(log, [ + isMethodCall('initialize', arguments: { + 'requestAlertPermission': true, + 'requestSoundPermission': true, + 'requestBadgePermission': true, + 'requestProvisionalPermission': false, + 'requestCriticalPermission': false, + 'requestProvidesAppNotificationSettings': false, + 'defaultPresentAlert': true, + 'defaultPresentSound': true, + 'defaultPresentBadge': true, + 'defaultPresentBanner': true, + 'defaultPresentList': true, + 'notificationCategories': [], + // Note: requestCarPlayPermission should NOT be present + }) + ]); + }); + + test('IOSInitializationSettings inherits from DarwinInitializationSettings', () async { + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings( + requestAlertPermission: false, + requestSoundPermission: false, + requestBadgePermission: false, + requestCarPlayPermission: true, + ); + + // Test that IOSInitializationSettings is a DarwinInitializationSettings + expect(iosInitializationSettings, isA()); + + // Test that CarPlay permission is specific to iOS + expect(iosInitializationSettings.requestCarPlayPermission, true); + expect(iosInitializationSettings.requestAlertPermission, false); + expect(iosInitializationSettings.requestSoundPermission, false); + expect(iosInitializationSettings.requestBadgePermission, false); + }); + + test('show without iOS-specific details', () async { + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings(); const InitializationSettings initializationSettings = InitializationSettings(iOS: iosInitializationSettings); await flutterLocalNotificationsPlugin.initialize(initializationSettings); @@ -220,8 +294,8 @@ void main() { }); test('show with iOS-specific details', () async { - const DarwinInitializationSettings iosInitializationSettings = - DarwinInitializationSettings(); + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings(); const InitializationSettings initializationSettings = InitializationSettings(iOS: iosInitializationSettings); await flutterLocalNotificationsPlugin.initialize(initializationSettings); @@ -284,8 +358,8 @@ void main() { for (final RepeatInterval repeatInterval in RepeatInterval.values) { test('$repeatInterval', () async { await withClock(Clock.fixed(now), () async { - const DarwinInitializationSettings iosInitializationSettings = - DarwinInitializationSettings(); + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings(); const InitializationSettings initializationSettings = InitializationSettings(iOS: iosInitializationSettings); await flutterLocalNotificationsPlugin @@ -364,8 +438,8 @@ void main() { const Duration thirtySeconds = Duration(seconds: 30); test('$thirtySeconds', () async { await withClock(Clock.fixed(now), () async { - const DarwinInitializationSettings iosInitializationSettings = - DarwinInitializationSettings(); + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings(); const InitializationSettings initializationSettings = InitializationSettings(iOS: iosInitializationSettings); await flutterLocalNotificationsPlugin @@ -411,8 +485,8 @@ void main() { for (final Duration repeatDurationInterval in repeatDurationIntervals) { test('$repeatDurationInterval', () async { await withClock(Clock.fixed(now), () async { - const DarwinInitializationSettings iosInitializationSettings = - DarwinInitializationSettings(); + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings(); const InitializationSettings initializationSettings = InitializationSettings(iOS: iosInitializationSettings); await flutterLocalNotificationsPlugin @@ -488,8 +562,8 @@ void main() { group('zonedSchedule', () { test('no repeat frequency', () async { - const DarwinInitializationSettings iosInitializationSettings = - DarwinInitializationSettings(); + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings(); const InitializationSettings initializationSettings = InitializationSettings(iOS: iosInitializationSettings); await flutterLocalNotificationsPlugin @@ -556,8 +630,8 @@ void main() { }); test('match time components', () async { - const DarwinInitializationSettings iosInitializationSettings = - DarwinInitializationSettings(); + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings(); const InitializationSettings initializationSettings = InitializationSettings(iOS: iosInitializationSettings); await flutterLocalNotificationsPlugin @@ -626,8 +700,8 @@ void main() { }); test('match day of week and time components', () async { - const DarwinInitializationSettings iosInitializationSettings = - DarwinInitializationSettings(); + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings(); const InitializationSettings initializationSettings = InitializationSettings(iOS: iosInitializationSettings); await flutterLocalNotificationsPlugin @@ -708,6 +782,7 @@ void main() { 'alert': false, 'provisional': false, 'critical': false, + 'carPlay': false, 'providesAppNotificationSettings': false }) ]); @@ -730,6 +805,7 @@ void main() { 'alert': true, 'provisional': true, 'critical': true, + 'carPlay': false, 'providesAppNotificationSettings': true, }) ]); @@ -781,8 +857,8 @@ void main() { }); test('initialize with providesAppNotificationSettings', () async { - const DarwinInitializationSettings iosInitializationSettings = - DarwinInitializationSettings( + const IOSInitializationSettings iosInitializationSettings = + IOSInitializationSettings( requestProvidesAppNotificationSettings: true, ); const InitializationSettings initializationSettings = @@ -797,6 +873,7 @@ void main() { 'requestBadgePermission': true, 'requestProvisionalPermission': false, 'requestCriticalPermission': false, + 'requestCarPlayPermission': false, 'requestProvidesAppNotificationSettings': true, 'defaultPresentAlert': true, 'defaultPresentSound': true, @@ -826,9 +903,32 @@ void main() { 'badge': true, 'provisional': false, 'critical': false, + 'carPlay': false, 'providesAppNotificationSettings': true }) ]); }); + + test('iOS requestPermissions with CarPlay enabled', () async { + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + IOSFlutterLocalNotificationsPlugin>()! + .requestPermissions( + sound: true, + alert: true, + badge: true, + carPlay: true); + expect(log, [ + isMethodCall('requestPermissions', arguments: { + 'sound': true, + 'alert': true, + 'badge': true, + 'provisional': false, + 'critical': false, + 'carPlay': true, + 'providesAppNotificationSettings': false + }) + ]); + }); }); } diff --git a/flutter_local_notifications/test/platform_specifics/darwin/mappers_test.dart b/flutter_local_notifications/test/platform_specifics/darwin/mappers_test.dart index 090dd4e38..021b3febb 100644 --- a/flutter_local_notifications/test/platform_specifics/darwin/mappers_test.dart +++ b/flutter_local_notifications/test/platform_specifics/darwin/mappers_test.dart @@ -5,6 +5,7 @@ import 'package:flutter_local_notifications/src/platform_specifics/darwin/notifi import 'package:flutter_local_notifications/src/platform_specifics/darwin/notification_action_option.dart'; import 'package:flutter_local_notifications/src/platform_specifics/darwin/notification_category.dart'; import 'package:flutter_local_notifications/src/platform_specifics/darwin/notification_category_option.dart'; +import 'package:flutter_local_notifications/src/platform_specifics/darwin/initialization_settings.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -44,4 +45,49 @@ void main() { ); }); }); + + group('IOSInitializationSettingsMapper', () { + test('should map CarPlay permission correctly', () { + const iosSettings = IOSInitializationSettings( + requestAlertPermission: true, + requestSoundPermission: true, + requestBadgePermission: true, + requestCarPlayPermission: true, + requestCriticalPermission: false, + requestProvisionalPermission: false, + requestProvidesAppNotificationSettings: false, + ); + + final map = iosSettings.toMap(); + + expect(map['requestAlertPermission'], true); + expect(map['requestSoundPermission'], true); + expect(map['requestBadgePermission'], true); + expect(map['requestCarPlayPermission'], true); + expect(map['requestCriticalPermission'], false); + expect(map['requestProvisionalPermission'], false); + expect(map['requestProvidesAppNotificationSettings'], false); + }); + + test('should inherit Darwin settings and add CarPlay permission', () { + const iosSettings = IOSInitializationSettings( + requestCarPlayPermission: true, + ); + + final map = iosSettings.toMap(); + + // Should have all Darwin defaults + expect(map['requestAlertPermission'], true); + expect(map['requestSoundPermission'], true); + expect(map['requestBadgePermission'], true); + expect(map['defaultPresentAlert'], true); + expect(map['defaultPresentSound'], true); + expect(map['defaultPresentBadge'], true); + expect(map['defaultPresentBanner'], true); + expect(map['defaultPresentList'], true); + + // Plus iOS-specific CarPlay + expect(map['requestCarPlayPermission'], true); + }); + }); }