Кнопки действий уведомления 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
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

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