Skip to content

Commit b54129f

Browse files
feat(iOS): Add CarPlay notification support with IOSInitializationSettings
1 parent 97dfbf1 commit b54129f

File tree

11 files changed

+328
-31
lines changed

11 files changed

+328
-31
lines changed

flutter_local_notifications/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ Note: the plugin requires Flutter SDK 3.22 at a minimum. The list of support pla
101101
* [Android] Start a foreground service
102102
* [Android] Ability to check if notifications are enabled
103103
* [iOS (all supported versions) & macOS 10.14+] Request notification permissions and customise the permissions being requested around displaying notifications
104+
* [iOS 10+] Request CarPlay notification permissions for notifications to appear in CarPlay interface
104105
* [iOS 10 or newer and macOS 10.14 or newer] Display notifications with attachments
105106
* [iOS 12.0+] Support for custom notification settings UI via "Configure Notifications in <application name>" button in notification context menu (API available on macOS 10.14+ but UI button does not appear in practice)
106107
* [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
674675

675676
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.
676677

678+
### [iOS-specific] Using IOSInitializationSettings
679+
680+
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.
681+
682+
```dart
683+
const AndroidInitializationSettings initializationSettingsAndroid =
684+
AndroidInitializationSettings('app_icon');
685+
final IOSInitializationSettings initializationSettingsIOS =
686+
IOSInitializationSettings(
687+
// Darwin settings
688+
requestAlertPermission: true,
689+
requestBadgePermission: true,
690+
requestSoundPermission: true,
691+
// iOS-specific settings
692+
requestCarPlayPermission: true,
693+
);
694+
final DarwinInitializationSettings initializationSettingsDarwin =
695+
DarwinInitializationSettings();
696+
final InitializationSettings initializationSettings = InitializationSettings(
697+
android: initializationSettingsAndroid,
698+
iOS: initializationSettingsIOS, // Use iOS-specific settings
699+
macOS: initializationSettingsDarwin, // Keep Darwin for macOS
700+
);
701+
```
702+
703+
#### CarPlay Integration
704+
705+
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.
706+
707+
**Important considerations:**
708+
- CarPlay permissions are only available on iOS 10.0 and later
709+
- The permission request will be silently ignored on unsupported devices
710+
- CarPlay notifications follow the same content guidelines as regular iOS notifications
711+
- Test CarPlay integration using the iOS Simulator's CarPlay mode
712+
713+
#### Migration from DarwinInitializationSettings
714+
715+
Existing code using `DarwinInitializationSettings` for iOS will continue to work without changes. To migrate to iOS-specific features:
716+
717+
1. Replace `DarwinInitializationSettings` with `IOSInitializationSettings` in your iOS initialization
718+
2. Add any iOS-specific options like `requestCarPlayPermission`
719+
3. Keep using `DarwinInitializationSettings` for macOS initialization
720+
721+
```dart
722+
// Before (still works)
723+
final DarwinInitializationSettings initializationSettingsDarwin =
724+
DarwinInitializationSettings(requestAlertPermission: true);
725+
726+
// After (with iOS-specific features)
727+
final IOSInitializationSettings initializationSettingsIOS =
728+
IOSInitializationSettings(
729+
requestAlertPermission: true,
730+
requestCarPlayPermission: true,
731+
);
732+
```
733+
677734
```dart
678735
void onDidReceiveNotificationResponse(NotificationResponse notificationResponse) async {
679736
final String? payload = notificationResponse.payload;

flutter_local_notifications/example/integration_test/flutter_local_notifications_test.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ void main() {
88
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
99
const AndroidInitializationSettings initializationSettingsAndroid =
1010
AndroidInitializationSettings('app_icon');
11-
const DarwinInitializationSettings initializationSettingsIOS =
12-
DarwinInitializationSettings();
11+
const IOSInitializationSettings initializationSettingsIOS =
12+
IOSInitializationSettings();
1313
const DarwinInitializationSettings initializationSettingsMacOS =
1414
DarwinInitializationSettings();
1515
final LinuxInitializationSettings initializationSettingsLinux =

flutter_local_notifications/example/lib/main.dart

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,14 @@ Future<void> main() async {
136136

137137
/// Note: permissions aren't requested here just to demonstrate that can be
138138
/// done later
139-
final DarwinInitializationSettings initializationSettingsDarwin =
139+
final IOSInitializationSettings initializationSettingsIOS =
140+
IOSInitializationSettings(
141+
requestAlertPermission: false,
142+
requestBadgePermission: false,
143+
requestSoundPermission: false,
144+
notificationCategories: darwinNotificationCategories,
145+
);
146+
final DarwinInitializationSettings initializationSettingsMacOS =
140147
DarwinInitializationSettings(
141148
requestAlertPermission: false,
142149
requestBadgePermission: false,
@@ -152,8 +159,8 @@ Future<void> main() async {
152159

153160
final InitializationSettings initializationSettings = InitializationSettings(
154161
android: initializationSettingsAndroid,
155-
iOS: initializationSettingsDarwin,
156-
macOS: initializationSettingsDarwin,
162+
iOS: initializationSettingsIOS,
163+
macOS: initializationSettingsMacOS,
157164
linux: initializationSettingsLinux,
158165
windows: windows.initSettings,
159166
);

flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/FlutterLocalNotificationsPlugin.m

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ @implementation FlutterLocalNotificationsPlugin {
4949
NSString *const REQUEST_PROVISIONAL_PERMISSION =
5050
@"requestProvisionalPermission";
5151
NSString *const REQUEST_CRITICAL_PERMISSION = @"requestCriticalPermission";
52+
NSString *const REQUEST_CARPLAY_PERMISSION = @"requestCarPlayPermission";
5253
NSString *const REQUEST_PROVIDES_APP_NOTIFICATION_SETTINGS =
5354
@"requestProvidesAppNotificationSettings";
5455
NSString *const DEFAULT_PRESENT_ALERT = @"defaultPresentAlert";
@@ -112,6 +113,7 @@ @implementation FlutterLocalNotificationsPlugin {
112113
NSString *const IS_CRITICAL_ENABLED = @"isCriticalEnabled";
113114
NSString *const IS_PROVIDES_APP_NOTIFICATION_SETTINGS_ENABLED =
114115
@"isProvidesAppNotificationSettingsEnabled";
116+
NSString *const IS_CAR_PLAY_ENABLED = @"isCarPlayEnabled";
115117

116118
NSString *const CRITICAL_SOUND_VOLUME = @"criticalSoundVolume";
117119

@@ -353,6 +355,8 @@ - (void)initialize:(NSDictionary *_Nonnull)arguments
353355
bool requestedBadgePermission = false;
354356
bool requestedProvisionalPermission = false;
355357
bool requestedCriticalPermission = false;
358+
bool requestedCarPlayPermission = false;
359+
bool carPlayPermissionSpecified = false;
356360
bool requestedProvidesAppNotificationSettings = false;
357361
NSMutableDictionary *presentationOptions = [[NSMutableDictionary alloc] init];
358362
if ([self containsKey:DEFAULT_PRESENT_ALERT forDictionary:arguments]) {
@@ -401,6 +405,11 @@ - (void)initialize:(NSDictionary *_Nonnull)arguments
401405
requestedCriticalPermission =
402406
[arguments[REQUEST_CRITICAL_PERMISSION] boolValue];
403407
}
408+
if ([self containsKey:REQUEST_CARPLAY_PERMISSION forDictionary:arguments]) {
409+
carPlayPermissionSpecified = true;
410+
requestedCarPlayPermission =
411+
[arguments[REQUEST_CARPLAY_PERMISSION] boolValue];
412+
}
404413
if ([self containsKey:REQUEST_PROVIDES_APP_NOTIFICATION_SETTINGS
405414
forDictionary:arguments]) {
406415
requestedProvidesAppNotificationSettings =
@@ -426,6 +435,8 @@ - (void)initialize:(NSDictionary *_Nonnull)arguments
426435
requestedProvisionalPermission
427436
criticalPermission:
428437
requestedCriticalPermission
438+
carPlayPermission:requestedCarPlayPermission
439+
carPlayPermissionSpecified:carPlayPermissionSpecified
429440
providesAppNotificationSettings:
430441
requestedProvidesAppNotificationSettings
431442
result:result];
@@ -440,6 +451,8 @@ - (void)requestPermissions:(NSDictionary *_Nonnull)arguments
440451
bool badgePermission = false;
441452
bool provisionalPermission = false;
442453
bool criticalPermission = false;
454+
bool requestCarPlayPermission = false;
455+
bool carPlayPermissionSpecified = false;
443456
bool providesAppNotificationSettings = false;
444457
if ([self containsKey:SOUND_PERMISSION forDictionary:arguments]) {
445458
soundPermission = [arguments[SOUND_PERMISSION] boolValue];
@@ -456,6 +469,12 @@ - (void)requestPermissions:(NSDictionary *_Nonnull)arguments
456469
if ([self containsKey:CRITICAL_PERMISSION forDictionary:arguments]) {
457470
criticalPermission = [arguments[CRITICAL_PERMISSION] boolValue];
458471
}
472+
if ([self containsKey:REQUEST_CARPLAY_PERMISSION forDictionary:arguments]) {
473+
carPlayPermissionSpecified = true;
474+
requestCarPlayPermission =
475+
[arguments[REQUEST_CARPLAY_PERMISSION] boolValue];
476+
477+
}
459478
if ([self containsKey:PROVIDES_APP_NOTIFICATION_SETTINGS
460479
forDictionary:arguments]) {
461480
providesAppNotificationSettings =
@@ -466,6 +485,8 @@ - (void)requestPermissions:(NSDictionary *_Nonnull)arguments
466485
badgePermission:badgePermission
467486
provisionalPermission:provisionalPermission
468487
criticalPermission:criticalPermission
488+
carPlayPermission:requestCarPlayPermission
489+
carPlayPermissionSpecified:carPlayPermissionSpecified
469490
providesAppNotificationSettings:providesAppNotificationSettings
470491
result:result];
471492
}
@@ -475,6 +496,8 @@ - (void)requestPermissionsImpl:(bool)soundPermission
475496
badgePermission:(bool)badgePermission
476497
provisionalPermission:(bool)provisionalPermission
477498
criticalPermission:(bool)criticalPermission
499+
carPlayPermission:(bool)carPlayPermission
500+
carPlayPermissionSpecified:(bool)carPlayPermissionSpecified
478501
providesAppNotificationSettings:(bool)providesAppNotificationSettings
479502
result:(FlutterResult _Nonnull)result {
480503
if (!soundPermission && !alertPermission && !badgePermission &&
@@ -495,6 +518,11 @@ - (void)requestPermissionsImpl:(bool)soundPermission
495518
if (badgePermission) {
496519
authorizationOptions += UNAuthorizationOptionBadge;
497520
}
521+
if (@available(iOS 10.0, *)) {
522+
if (carPlayPermissionSpecified && carPlayPermission) {
523+
authorizationOptions += UNAuthorizationOptionCarPlay;
524+
}
525+
}
498526
if (@available(iOS 12.0, *)) {
499527
if (provisionalPermission) {
500528
authorizationOptions += UNAuthorizationOptionProvisional;
@@ -530,6 +558,7 @@ - (void)checkPermissions:(NSDictionary *_Nonnull)arguments
530558
BOOL isProvisionalEnabled = false;
531559
BOOL isCriticalEnabled = false;
532560
BOOL isProvidesAppNotificationSettingsEnabled = false;
561+
BOOL isCarPlayEnabled = false;
533562

534563
if (@available(iOS 12.0, *)) {
535564
isProvisionalEnabled =
@@ -540,6 +569,11 @@ - (void)checkPermissions:(NSDictionary *_Nonnull)arguments
540569
settings.providesAppNotificationSettings;
541570
}
542571

572+
if (@available(iOS 10.0, *)) {
573+
isCarPlayEnabled =
574+
settings.carPlaySetting == UNNotificationSettingEnabled;
575+
}
576+
543577
NSDictionary *dict = @{
544578
IS_NOTIFICATIONS_ENABLED : @(isEnabled),
545579
IS_SOUND_ENABLED : @(isSoundEnabled),
@@ -549,6 +583,7 @@ - (void)checkPermissions:(NSDictionary *_Nonnull)arguments
549583
IS_CRITICAL_ENABLED : @(isCriticalEnabled),
550584
IS_PROVIDES_APP_NOTIFICATION_SETTINGS_ENABLED :
551585
@(isProvidesAppNotificationSettingsEnabled),
586+
IS_CAR_PLAY_ENABLED : @(isCarPlayEnabled),
552587
};
553588

554589
result(dict);

flutter_local_notifications/lib/src/initialization_settings.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ class InitializationSettings {
2121
///
2222
/// It is nullable, because we don't want to force users to specify settings
2323
/// for platforms that they don't target.
24+
///
25+
/// You can pass either DarwinInitializationSettings or
26+
/// IOSInitializationSettings. Use IOSInitializationSettings to access
27+
/// iOS-specific features like CarPlay.
2428
final DarwinInitializationSettings? iOS;
2529

2630
/// Settings for macOS.

flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -648,10 +648,14 @@ class IOSFlutterLocalNotificationsPlugin
648648

649649
DidReceiveNotificationResponseCallback? _onDidReceiveNotificationResponse;
650650

651-
/// Initializes the plugin.
651+
/// Initializes the plugin for iOS.
652652
///
653653
/// Call this method on application before using the plugin further.
654654
///
655+
/// Accepts either [DarwinInitializationSettings] for basic Darwin-based
656+
/// configuration or [IOSInitializationSettings] for iOS-specific features
657+
/// like CarPlay notifications.
658+
///
655659
/// Initialisation may also request notification permissions where users will
656660
/// see a permissions prompt. This may be fine in cases where it's acceptable
657661
/// to do this when the application runs for the first time. However, if your
@@ -664,6 +668,11 @@ class IOSFlutterLocalNotificationsPlugin
664668
/// [requestPermissions] can then be called to request permissions when
665669
/// needed.
666670
///
671+
/// When using [IOSInitializationSettings], CarPlay notifications can be
672+
/// enabled by setting [IOSInitializationSettings.requestCarPlayPermission]
673+
/// to true. When using [DarwinInitializationSettings], CarPlay is disabled
674+
/// by default.
675+
///
667676
/// The [onDidReceiveNotificationResponse] callback is fired when the user
668677
/// selects a notification or notification action that should show the
669678
/// application/user interface.
@@ -684,7 +693,17 @@ class IOSFlutterLocalNotificationsPlugin
684693
_onDidReceiveNotificationResponse = onDidReceiveNotificationResponse;
685694
_channel.setMethodCallHandler(_handleMethod);
686695

687-
final Map<String, Object> arguments = initializationSettings.toMap();
696+
// Convert to map using appropriate mapper based on runtime type
697+
// IOSInitializationSettings.toMap() automatically includes CarPlay field
698+
// DarwinInitializationSettings.toMap() does not include CarPlay field
699+
final Map<String, Object> arguments;
700+
if (initializationSettings is IOSInitializationSettings) {
701+
// Explicitly call iOS mapper extension
702+
arguments = (initializationSettings as IOSInitializationSettings).toMap();
703+
} else {
704+
// Use Darwin mapper for DarwinInitializationSettings
705+
arguments = initializationSettings.toMap();
706+
}
688707

689708
_evaluateBackgroundNotificationCallback(
690709
onDidReceiveBackgroundNotificationResponse, arguments);
@@ -694,12 +713,17 @@ class IOSFlutterLocalNotificationsPlugin
694713

695714
/// Requests the specified permission(s) from user and returns current
696715
/// permission status.
716+
///
717+
/// On iOS, the [carPlay] parameter requests permission to show notifications
718+
/// on CarPlay when connected to a compatible vehicle. This requires iOS 10.0+
719+
/// and is only applicable to iOS devices.
697720
Future<bool?> requestPermissions({
698721
bool sound = false,
699722
bool alert = false,
700723
bool badge = false,
701724
bool provisional = false,
702725
bool critical = false,
726+
bool carPlay = false,
703727
bool providesAppNotificationSettings = false,
704728
}) =>
705729
_channel.invokeMethod<bool?>('requestPermissions', <String, bool>{
@@ -708,6 +732,7 @@ class IOSFlutterLocalNotificationsPlugin
708732
'badge': badge,
709733
'provisional': provisional,
710734
'critical': critical,
735+
'carPlay': carPlay,
711736
'providesAppNotificationSettings': providesAppNotificationSettings,
712737
});
713738

@@ -730,6 +755,7 @@ class IOSFlutterLocalNotificationsPlugin
730755
isCriticalEnabled: dict['isCriticalEnabled'] ?? false,
731756
isProvidesAppNotificationSettingsEnabled:
732757
dict['isProvidesAppNotificationSettingsEnabled'] ?? false,
758+
isCarPlayEnabled: dict['isCarPlayEnabled'] ?? false,
733759
);
734760
},
735761
);

flutter_local_notifications/lib/src/platform_specifics/darwin/initialization_settings.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,34 @@ class DarwinInitializationSettings {
160160
/// On macOS, this is only applicable to macOS 10.14 or newer.
161161
final List<DarwinNotificationCategory> notificationCategories;
162162
}
163+
164+
/// Plugin initialization settings for iOS
165+
class IOSInitializationSettings extends DarwinInitializationSettings {
166+
/// Constructs an instance of [IOSInitializationSettings].
167+
const IOSInitializationSettings({
168+
super.requestAlertPermission = true,
169+
super.requestSoundPermission = true,
170+
super.requestBadgePermission = true,
171+
super.requestProvisionalPermission = false,
172+
super.requestCriticalPermission = false,
173+
super.requestProvidesAppNotificationSettings = false,
174+
this.requestCarPlayPermission = false,
175+
super.defaultPresentAlert = true,
176+
super.defaultPresentSound = true,
177+
super.defaultPresentBadge = true,
178+
super.defaultPresentBanner = true,
179+
super.defaultPresentList = true,
180+
super.notificationCategories = const <DarwinNotificationCategory>[],
181+
});
182+
183+
/// Request permission to show notifications on CarPlay.
184+
///
185+
/// This permission allows the app to display notifications on the CarPlay
186+
/// interface when connected to a compatible vehicle.
187+
///
188+
/// Default value is false.
189+
///
190+
/// This property is only applicable to iOS 10.0 or newer and is iOS-only.
191+
/// CarPlay is not available on macOS.
192+
final bool requestCarPlayPermission;
193+
}

flutter_local_notifications/lib/src/platform_specifics/darwin/mappers.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ extension DarwinInitializationSettingsMapper on DarwinInitializationSettings {
5151
};
5252
}
5353

54+
extension IOSInitializationSettingsMapper on IOSInitializationSettings {
55+
Map<String, Object> toMap() {
56+
final DarwinInitializationSettings darwinSettings =
57+
this as DarwinInitializationSettings;
58+
final Map<String, Object> map = darwinSettings.toMap();
59+
map['requestCarPlayPermission'] = requestCarPlayPermission;
60+
return map;
61+
}
62+
}
63+
5464
extension on DarwinNotificationAttachmentThumbnailClippingRect {
5565
Map<String, Object> toMap() => <String, Object>{
5666
'x': x,

flutter_local_notifications/lib/src/platform_specifics/darwin/notification_enabled_options.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class NotificationsEnabledOptions {
1111
required this.isProvisionalEnabled,
1212
required this.isCriticalEnabled,
1313
required this.isProvidesAppNotificationSettingsEnabled,
14+
this.isCarPlayEnabled = false,
1415
});
1516

1617
/// Whenever notifications are enabled.
@@ -44,4 +45,9 @@ class NotificationsEnabledOptions {
4445
/// On macOS, the permission can be requested and this value reports
4546
/// correctly, but the UI button does not appear in practice.
4647
final bool isProvidesAppNotificationSettingsEnabled;
48+
49+
/// Whether CarPlay notifications are enabled.
50+
///
51+
/// Available on iOS 10.0 and later.
52+
final bool isCarPlayEnabled;
4753
}

0 commit comments

Comments
 (0)