Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions flutter_local_notifications/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <application name>" 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
Expand Down Expand Up @@ -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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- CarPlay permissions are only available on iOS 10.0 and later
- CarPlay permissions are only available on iOS 10.0 or newer

For consistency with rest of plugin docs that have been using "or newer"

- 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
13 changes: 10 additions & 3 deletions flutter_local_notifications/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,14 @@ Future<void> 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,
Expand All @@ -152,8 +159,8 @@ Future<void> main() async {

final InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsDarwin,
macOS: initializationSettingsDarwin,
iOS: initializationSettingsIOS,
macOS: initializationSettingsMacOS,
linux: initializationSettingsLinux,
windows: windows.initSettings,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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]) {
Expand Down Expand Up @@ -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 =
Expand All @@ -426,6 +436,8 @@ - (void)initialize:(NSDictionary *_Nonnull)arguments
requestedProvisionalPermission
criticalPermission:
requestedCriticalPermission
carPlayPermission:requestedCarPlayPermission
carPlayPermissionSpecified:carPlayPermissionSpecified
providesAppNotificationSettings:
requestedProvidesAppNotificationSettings
result:result];
Expand All @@ -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];
Expand All @@ -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];

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove blank line

}
if ([self containsKey:PROVIDES_APP_NOTIFICATION_SETTINGS
forDictionary:arguments]) {
providesAppNotificationSettings =
Expand All @@ -466,6 +486,8 @@ - (void)requestPermissions:(NSDictionary *_Nonnull)arguments
badgePermission:badgePermission
provisionalPermission:provisionalPermission
criticalPermission:criticalPermission
carPlayPermission:requestCarPlayPermission
carPlayPermissionSpecified:carPlayPermissionSpecified
providesAppNotificationSettings:providesAppNotificationSettings
result:result];
}
Expand All @@ -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 &&
Expand All @@ -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;
Expand Down Expand Up @@ -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 =
Expand All @@ -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),
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -648,10 +648,14 @@

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
Expand All @@ -664,6 +668,11 @@
/// [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.
Expand All @@ -684,7 +693,17 @@
_onDidReceiveNotificationResponse = onDidReceiveNotificationResponse;
_channel.setMethodCallHandler(_handleMethod);

final Map<String, Object> 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<String, Object> arguments;
if (initializationSettings is IOSInitializationSettings) {
// Explicitly call iOS mapper extension
arguments = (initializationSettings as IOSInitializationSettings).toMap();

Check notice on line 702 in flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart

View workflow job for this annotation

GitHub Actions / Run Dart Analyzer

Unnecessary cast.

Try removing the cast. See https://dart.dev/diagnostics/unnecessary_cast to learn more about this problem.
} else {
// Use Darwin mapper for DarwinInitializationSettings
arguments = initializationSettings.toMap();
}

_evaluateBackgroundNotificationCallback(
onDidReceiveBackgroundNotificationResponse, arguments);
Expand All @@ -694,12 +713,17 @@

/// 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<bool?> requestPermissions({
bool sound = false,
bool alert = false,
bool badge = false,
bool provisional = false,
bool critical = false,
bool carPlay = false,
bool providesAppNotificationSettings = false,
}) =>
_channel.invokeMethod<bool?>('requestPermissions', <String, bool>{
Expand All @@ -708,6 +732,7 @@
'badge': badge,
'provisional': provisional,
'critical': critical,
'carPlay': carPlay,
'providesAppNotificationSettings': providesAppNotificationSettings,
});

Expand All @@ -730,6 +755,7 @@
isCriticalEnabled: dict['isCriticalEnabled'] ?? false,
isProvidesAppNotificationSettingsEnabled:
dict['isProvidesAppNotificationSettingsEnabled'] ?? false,
isCarPlayEnabled: dict['isCarPlayEnabled'] ?? false,
);
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,34 @@ class DarwinInitializationSettings {
/// On macOS, this is only applicable to macOS 10.14 or newer.
final List<DarwinNotificationCategory> 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 <DarwinNotificationCategory>[],
});

/// 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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ extension DarwinInitializationSettingsMapper on DarwinInitializationSettings {
};
}

extension IOSInitializationSettingsMapper on IOSInitializationSettings {
Map<String, Object> toMap() {
final DarwinInitializationSettings darwinSettings =
this as DarwinInitializationSettings;
final Map<String, Object> map = darwinSettings.toMap();
map['requestCarPlayPermission'] = requestCarPlayPermission;
return map;
}
}

extension on DarwinNotificationAttachmentThumbnailClippingRect {
Map<String, Object> toMap() => <String, Object>{
'x': x,
Expand Down
Loading
Loading