Кнопки действий уведомления Flutter Firebase не отображаются или не работают на iOSIOS

Программируем под IOS
Anonymous
Кнопки действий уведомления Flutter Firebase не отображаются или не работают на iOS

Сообщение Anonymous »

Я интегрировал уведомления Firebase в свое приложение Flutter, затем мне нужно добавить в него кнопку действий, и я добавил следующий класс службы уведомлений для уведомлений с кнопками действий, и эти кнопки и их действия работают нормально в завершенном и фоновом режиме, но в iOS они не работают. Я выполняю действие на основе типа в заставке, но в iOS ни кнопки не отображаются, ни действия не выполняются.

Ниже приведены мои службы уведомлений и код заставки

class NotificationService {
static final NotificationService _instance = NotificationService._internal();

factory NotificationService() => _instance;

NotificationService._internal();

final FlutterLocalNotificationsPlugin _localNotifications =
FlutterLocalNotificationsPlugin();
Function(String actionId, Map? payload)? onActionTapped;
isolates)
Future initialize({
bool forceReinit = false,
DidReceiveBackgroundNotificationResponseCallback?
onDidReceiveBackgroundNotificationResponse,
}) async {

if (Platform.isIOS) {
// 1. Invitation category - Accept/Reject buttons
iosCategories.add(
DarwinNotificationCategory(
'invitation_category',
actions: [
DarwinNotificationAction.plain(
'accept_invitation',
AppStrings.txtAccept,
options: {DarwinNotificationActionOption.foreground},
),
DarwinNotificationAction.plain(
'reject_invitation',
AppStrings.txtReject,
options: {DarwinNotificationActionOption.destructive},
),
],
options: {DarwinNotificationCategoryOption.hiddenPreviewShowTitle},
),
);

}

// iOS initialization settings with categories
final DarwinInitializationSettings iosSettings =
DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
notificationCategories: iosCategories,
);

final InitializationSettings initSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);

// Initialize the plugin
await _localNotifications.initialize(
initSettings,
onDidReceiveNotificationResponse: _onNotificationTapped,
onDidReceiveBackgroundNotificationResponse:
onDidReceiveBackgroundNotificationResponse,
);

// Request permissions
if (Platform.isAndroid) {
await _requestAndroidPermissions();
} else if (Platform.isIOS) {
await _requestIOSPermissions();
}

// Create notification channels for Android
await _createNotificationChannels();

_isInitialized = true;
debugPrint('✅ NotificationService initialized successfully');

// Verify initialization worked
try {
if (Platform.isAndroid) {
final androidImplementation =
_localNotifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
if (androidImplementation != null) {
debugPrint('✅ Android notification plugin ready');
}
} else if (Platform.isIOS) {
final iosImplementation =
_localNotifications
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin
>();
if (iosImplementation != null) {
debugPrint('✅ iOS notification plugin ready');
}
}
} catch (e) {
debugPrint('⚠️ Warning: Could not verify notification plugin: $e');
}
}

/// Request Android notification permissions
Future _requestAndroidPermissions() async {
if (Platform.isAndroid) {
final AndroidFlutterLocalNotificationsPlugin? androidImplementation =
_localNotifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();

if (androidImplementation != null) {
final bool? granted =
await androidImplementation.requestNotificationsPermission();
debugPrint('Android notification permission granted: $granted');
}
}
}

final bool? granted = await iosImplementation.requestPermissions(
alert: true,
badge: true,
sound: true,
);
debugPrint('iOS notification permission granted: $granted');
}

Future _createNotificationChannels() async {
if (Platform.isAndroid) {
final AndroidFlutterLocalNotificationsPlugin? androidImplementation =
_localNotifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();

if (androidImplementation != null) {
// Invitation channel
await androidImplementation.createNotificationChannel(
const AndroidNotificationChannel(
'invitation_channel',
AppStrings.txtInvitationNotifications,
description: AppStrings.txtNotificationsForCommitteeInvitations,
importance: Importance.high,
playSound: true,
enableVibration: true,
),
);

// Deposit reminder channel
await androidImplementation.createNotificationChannel(
const AndroidNotificationChannel(
'deposit_reminder_channel',
AppStrings.txtDepositReminders,
description: AppStrings.txtRemindersToDepositMoney,
importance: Importance.high,
playSound: true,
enableVibration: true,
),
);

// Host alerts channel
await androidImplementation.createNotificationChannel(
const AndroidNotificationChannel(
'host_alerts_channel',
AppStrings.txtHostAlerts,
description: AppStrings.txtNotificationsForCommitteeHosts,
importance: Importance.high,
playSound: true,
enableVibration: true,
),
);

}
}

/// Handle notification tap and action button taps
void _onNotificationTapped(NotificationResponse response) {
debugPrint('Notification tapped: ${response.id}');
debugPrint('Action ID: ${response.actionId}');
debugPrint('Payload: ${response.payload}');

if (response.actionId != null) {
// Action button was tapped
Map? payload;
if (response.payload != null) {
try {
// Try to parse payload as JSON string first
if (response.payload is String) {
final parsed =
jsonDecode(response.payload as String) as Map;
payload = parsed;
} else {
payload = {'data': response.payload};
}
} catch (e) {
// If parsing fails, wrap it in a data object
debugPrint('Error parsing payload: $e');
payload = {'data': response.payload};
}
}
payload ??= {};
payload['notification_id'] = response.id;
onActionTapped?.call(response.actionId!, payload);
} else {
// Notification body was tapped
Map? payload;
if (response.payload != null) {
try {
// Try to parse payload as JSON string
if (response.payload is String) {
final parsed =
jsonDecode(response.payload as String) as Map;
payload = parsed;
} else {
payload = {'id': response.id};
}
} catch (e) {
payload = {'id': response.id};
}
} else {
payload = {'id': response.id};
}
payload['notification_id'] = response.id;
onActionTapped?.call('notification_tapped', payload);
}
}

/// Process the notification/action that launched the app from terminated state.
Future handleAppLaunchNotificationAction() async {
final NotificationAppLaunchDetails? launchDetails =
await _localNotifications.getNotificationAppLaunchDetails();

if (launchDetails == null || !launchDetails.didNotificationLaunchApp) {
return;
}

final NotificationResponse? response = launchDetails.notificationResponse;
if (response != null) {
_onNotificationTapped(response);
}
}

/// Show a local notification with action buttons (for testing)
Future showInvitationNotification({
required String title,
required String body,
required String committeeId,
required String committeeTitle,
String? invitationId,
}) async {
const AndroidNotificationDetails androidDetails =
AndroidNotificationDetails(
'invitation_channel',
AppStrings.txtInvitationNotifications,
channelDescription: AppStrings.txtNotificationsForCommitteeInvitations,
importance: Importance.high,
priority: Priority.high,
showWhen: true,
// Define action buttons
actions: [
AndroidNotificationAction(
'accept_invitation',
AppStrings.txtAccept,
titleColor: Color(0xFF2196F3), // Blue color
showsUserInterface: true,
),
AndroidNotificationAction(
'reject_invitation',
AppStrings.txtReject,
titleColor: Color(0xFF757575), // Grey color
showsUserInterface: true,
),
],
);

const DarwinNotificationDetails iosDetails = DarwinNotificationDetails(
categoryIdentifier: 'invitation_category',
presentAlert: true,
presentBanner: true,
presentBadge: true,
presentList: true,
presentSound: true,
);

const NotificationDetails notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);

// Create payload with committee information and invitation ID
final String payload =
invitationId != null && invitationId.isNotEmpty
? '{"committee_id": "$committeeId", "committee_title": "$committeeTitle", "type": "invitation", "typeId": "$invitationId"}'
: '{"committee_id": "$committeeId", "committee_title": "$committeeTitle", "type": "invitation"}';

final int notificationId =
int.tryParse(invitationId ?? '') ??
DateTime.now().millisecondsSinceEpoch.remainder(100000);

await _localNotifications.show(
notificationId,
title,
body,
notificationDetails,
payload: payload,
);
}

/// Show chat message notification with Reply button
Future showChatNotification({
required String title,
required String body,
required String committeeId,
String? senderId,
String? senderName,
}) async {
const AndroidNotificationDetails androidDetails =
AndroidNotificationDetails(
'chat_channel',
AppStrings.txtChatNotifications,
channelDescription: AppStrings.txtNotificationsForChatMessages,
importance: Importance.high,
priority: Priority.high,
showWhen: true,
actions: [
AndroidNotificationAction(
'reply',
AppStrings.txtReply,
titleColor: Color(0xFF2196F3),
showsUserInterface: true,
),
],
);

const DarwinNotificationDetails iosDetails = DarwinNotificationDetails(
categoryIdentifier: 'chat_category',
presentAlert: true,
presentBanner: true,
presentBadge: true,
presentList: true,
presentSound: true,
);

final String payload =
'{"committee_id": "$committeeId", "type": "chat", "sender_id": "${senderId ?? ""}", "sender_name": "${senderName ?? ""}"}';

await _localNotifications.show(
DateTime.now().millisecondsSinceEpoch.remainder(100000),
title,
body,
const NotificationDetails(android: androidDetails, iOS: iosDetails),
payload: payload,
);
}

/// Show onboarding reminder notification with Resume button
Future showOnboardingNotification({
required String title,
required String body,
String? step,
}) async {
const AndroidNotificationDetails androidDetails =
AndroidNotificationDetails(
'onboarding_channel',
AppStrings.txtOnboardingNotifications,
channelDescription: AppStrings.txtNotificationsForOnboardingReminders,
importance: Importance.high,
priority: Priority.high,
showWhen: true,
actions: [
AndroidNotificationAction(
'resume',
AppStrings.txtResume,
titleColor: Color(0xFF2196F3),
showsUserInterface: true,
),
],
);

const DarwinNotificationDetails iosDetails = DarwinNotificationDetails(
categoryIdentifier: 'onboarding_category',
presentAlert: true,
presentBanner: true,
presentBadge: true,
presentList: true,
presentSound: true,
);

final String payload = '{"type": "onboarding", "step": "${step ?? ""}"}';

await _localNotifications.show(
DateTime.now().millisecondsSinceEpoch.remainder(100000),
title,
body,
const NotificationDetails(android: androidDetails, iOS: iosDetails),
payload: payload,
);
}

/// Show change request notification with Accept/Reject buttons
Future showChangeRequestNotification({
required String title,
required String body,
required String committeeId,
String? requestId,
}) async {
const AndroidNotificationDetails androidDetails =
AndroidNotificationDetails(
'change_request_channel',
AppStrings.txtChangeRequestNotifications,
channelDescription: AppStrings.txtNotificationsForChangeRequests,
importance: Importance.high,
priority: Priority.high,
showWhen: true,
actions: [
AndroidNotificationAction(
'accept_change',
AppStrings.txtAccept,
titleColor: Color(0xFF2196F3),
showsUserInterface: true,
),
AndroidNotificationAction(
'reject_change',
AppStrings.txtReject,
titleColor: Color(0xFF757575),
showsUserInterface: true,
),
],
);

const DarwinNotificationDetails iosDetails = DarwinNotificationDetails(
categoryIdentifier: 'change_request_category',
presentAlert: true,
presentBanner: true,
presentBadge: true,
presentList: true,
presentSound: true,
);

final String payload =
'{"committee_id": "$committeeId", "type": "change_request", "request_id": "${requestId ?? ""}"}';

await _localNotifications.show(
DateTime.now().millisecondsSinceEpoch.remainder(100000),
title,
body,
const NotificationDetails(android: androidDetails, iOS: iosDetails),
payload: payload,
);
}

/// Handle FCM messages (foreground and background) and convert to local notifications with actions
Future handleForegroundMessage(RemoteMessage message) async {
try {
debugPrint('📨 Handling FCM message: ${message.messageId}');
debugPrint('📨 Message data: ${message.data}');
debugPrint(
'📨 Notification payload: ${message.notification?.title} - ${message.notification?.body}',
);

final data = message.data;
final type = data['type'] ?? 'general';
final typeID =
(data['type_id'] ??
data['typeId'] ??
data['invitation_id'] ??
data['invitationId'] ??
'')
.toString();

debugPrint("Payload DATA -->$data");

// Get title and body from notification payload or data payload
final String title =
message.notification?.title ??
data['title'] ??
data['notification_title'] ??
AppStrings.txtNewNotification;
final String body =
message.notification?.body ??
data['body'] ??
data['notification_body'] ??
AppStrings.txtYouHaveNewNotification;

final committeeId = data['committee_id'] ?? '';
final committeeTitle = data['committee_title'] ?? AppStrings.txtCommitteeDefault;

debugPrint('📨 Notification type: $type');
debugPrint('📨 Title: $title');
debugPrint('📨 "type Id": $typeID');
debugPrint('📨 Body: $body');
debugPrint('📨 Committee ID: $committeeId');

// Route to appropriate notification method based on type
switch (type) {
case 'invitation':
await showInvitationNotification(
title: title,
body: body,
committeeId: committeeId.toString(),
committeeTitle: committeeTitle,
invitationId: typeID.toString(),
);
break;

// case 'deposit_reminder':
case 'current_deposit_reminder':
await showDepositReminderNotification(
title: title,
body: body,
committeeId: committeeId.toString(),
);
break;

// case 'host_incomplete':
case 'pending_invitations':
case 'host_nudge_for_remind_to_join':
await showHostIncompleteNotification(
title: title,
body: body,
committeeId: committeeId.toString(),
type: type.toString(),
);
break;

// case 'host_rejection':
// case 'NOTIFICATION_INVITATION_REJECTED':
case 'invitee_interest_no':
case 'invitation_rejected':
await showHostRejectionNotification(
title: title,
body: body,
committeeId: committeeId.toString(),
type: type.toString(),
);
break;

// case 'host_interested':
case 'invitee_interest_yes':
await showHostInterestedNotification(
title: title,
body: body,
committeeId: committeeId.toString(),
);
break;

// case 'custom_push':
case 'custom_app_notifications':
final contactUsFlags = [
data['show_contact_us'],
data['contact_us_button'],
data['contact_us_enabled'],
data['add_contact_us'],
data['has_contact_us_cta'],
];
final hasExplicitContactUsFlag = contactUsFlags.any(
(value) => value != null && value.toString().trim().isNotEmpty,
);
// Backward compatible default: custom pushes show Contact Us unless
// backend explicitly sends one of the flags and all are false.
final showContactUsAction =
hasExplicitContactUsFlag ? contactUsFlags.any(_isTruthy) : true;

await showCustomPushNotification(
title: title,
body: body,
payload: data,
showContactUsAction: showContactUsAction,
);
break;

default:
// Show general notification without action buttons
await showGeneralNotification(
title: title,
body: body,
payload: data,
);
break;
}

debugPrint('✅ Successfully showed notification with type: $type');
} catch (e, stackTrace) {
debugPrint('❌ Error handling FCM message: $e');
debugPrint('❌ Stack trace: $stackTrace');
// Fallback: try to show a basic notification
try {
await showGeneralNotification(
title: message.notification?.title ?? AppStrings.txtNewNotification,
body: message.notification?.body ?? AppStrings.txtYouHaveNewNotification,
payload: message.data,
);
} catch (fallbackError) {
debugPrint('❌ Error showing fallback notification: $fallbackError');
}
}

}

/// Show a completion state for quick action API calls.
Future showActionResultNotification({
required int notificationId,
required String title,
required String body,
}) async {
if (!Platform.isAndroid) return;

const AndroidNotificationDetails androidDetails =
AndroidNotificationDetails(
'invitation_channel',
AppStrings.txtInvitationNotifications,
channelDescription: AppStrings.txtNotificationsForCommitteeInvitations,
importance: Importance.high,
priority: Priority.high,
showWhen: true,
onlyAlertOnce: true,
ongoing: false,
autoCancel: true,
);

await _localNotifications.show(
notificationId,
title,
body,
const NotificationDetails(android: androidDetails),
);
}

}



class SplashScreen extends StatefulWidget {

Future _navigateToNextScreen() async {
if (!mounted) return;

if (SplashScreen.isFromNotifications) {
final type = SplashScreen.notificationType;
final committeeId = SplashScreen.committeeID;

if (type == AppNotificationsType.NOTIFICATION_IHOST_NUDGE_TO_JOIN ||
type == AppNotificationsType.NOTIFICATION_PENDING_INVITES) {
SplashScreen.isFromNotificationRemindAll = true;
}
if (type == AppNotificationsType.NOTIFICATION_INVITATION_ACCEPTED ||
type == AppNotificationsType.NOTIFICATION_CURRENT_REMINDER ||
type == AppNotificationsType.NOTIFICATION_CONTACT_US ||
type == AppNotificationsType.NOTIFICATION_INVITATION_REJECTED ||
type == AppNotificationsType.NOTIFICATION_INVITEE_NO_NTEREST) {
// Treat accepted invitation the same as other lobby-related notifications
if (committeeId > 0) {
if (type == AppNotificationsType.NOTIFICATION_INVITATION_REJECTED ||
type == AppNotificationsType.NOTIFICATION_INVITEE_NO_NTEREST) {
SplashScreen.isFromNotificationLaunchInviteMoreUser = true;
}
if (type == AppNotificationsType.NOTIFICATION_CURRENT_REMINDER) {
SplashScreen.isFromAcceptInviteNotificationLaunchPayment = true;
}
if (type == AppNotificationsType.NOTIFICATION_PENDING_SETTLEMENTS ||
type ==
AppNotificationsType
.NOTIFICATION_PENDING_SETTLEMENTS_REMINDER ||
type ==
AppNotificationsType
.NOTIFICATION_PENDING_SETTLEMENTS_REMINDER_JOB) {
SplashScreen.isFromNotificationForSettlement = true;
}
SplashScreen.isFromAcceptInviteNotifications = true;
callForDashboard(clearStack: true);
} else {
callForDashboard(clearStack: true);
}
} else if (type == AppNotificationsType.NOTIFICATION_INVITATION ||
type == AppNotificationsType.NOTIFICATION_LOBBY ||
type == AppNotificationsType.NOTIFICATION_INVITATION_ACCEPTED ||
type == AppNotificationsType.NOTIFICATION_SWAP_REQUEST_ACCEPTED ||
type == AppNotificationsType.NOTIFICATION_COMMITTEE_CHAT ||
type == AppNotificationsType.NOTIFICATION_DISBAND_REQUEST ||
type == AppNotificationsType.NOTIFICATION_CONTACT_US ||
type == AppNotificationsType.NOTIFICATION_DISBAND_RESULT ||
type == AppNotificationsType.NOTIFICATION_INVITEE_NO_NTEREST ||
type == AppNotificationsType.NOTIFICATION_INVITATION_REJECTED ||
type == AppNotificationsType.NOTIFICATION_INVITEE_YES_NTEREST ||
type == AppNotificationsType.NOTIFICATION_IHOST_NUDGE_TO_JOIN ||
type == AppNotificationsType.NOTIFICATION_PENDING_INVITES ||
type == AppNotificationsType.NOTIFICATION_PENDING_SETTLEMENTS ||
type ==
AppNotificationsType.NOTIFICATION_PENDING_SETTLEMENTS_REMINDER ||
type ==
AppNotificationsType
.NOTIFICATION_PENDING_SETTLEMENTS_REMINDER_JOB ||
type == AppNotificationsType.NOTIFICATION_DISBAND_REQUEST_APPROVED) {
if (committeeId > 0) {
// Mark flag and let dashboard handle navigation when its context exists
SplashScreen.isFromAcceptInviteNotifications = true;
callForDashboard(clearStack: true);
} else {
// await callForLobby('471', clearStack: false);
callForDashboard(clearStack: true);
}
} else {
// await callForLobby('471', clearStack: true);
callForDashboard(clearStack: true);
}

return;
}

Navigator.of(
context,
).pushReplacement(MaterialPageRoute(builder: (_) => _getNextScreen()));
}

Future _setupFCM() async {
try {
// Get FCM token directly (no permission dialog)

final fcmToken = await FirebaseMessaging.instance.getToken();
if (fcmToken != null) {
await saveStringToPreferences(Constants.fcmToken, fcmToken);

print('✅ FCM Token saved: $fcmToken');
}

// Listen for token refresh
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) async {
await saveStringToPreferences(Constants.fcmToken, newToken);
print('🔄 FCM Token refreshed and saved: $newToken');
});

// Optional: handle background / terminated notifications
FirebaseMessaging.onMessageOpenedApp.listen((message) {
handleNotification(message);
});

FirebaseMessaging.instance.getInitialMessage().then((message) {
if (message != null) handleNotification(message);
});
} catch (e) {
print('❌ Error initializing FCM: $e');
}
initPlatformState();
}

void handleNotification(RemoteMessage message) {
final data = message.data;
final rawType = (data['type'] ?? '').toString();
final isContactUsType =
rawType == 'custom_app_notifications' ||
rawType == 'custom_push' ||
rawType == 'custom';
final type =
isContactUsType
? AppNotificationsType.NOTIFICATION_CONTACT_US
: rawType;
final committeeId = int.tryParse(data['committee_id'] ?? '');
SplashScreen.isFromNotifications = true;
SplashScreen.notificationType = type;
SplashScreen.committeeID = committeeId ?? 0;
if (isContactUsType) {
// Ensure dashboard consumes and opens Contact Us sheet after navigation.
saveBoolToPreferences(_pendingContactUsSheetKey, true);
}

}


Подробнее здесь: https://stackoverflow.com/questions/799 ... ing-on-ios

Вернуться в «IOS»